Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ jobs:
name: Build
runs-on: ubuntu-latest

env:
VITE_ENABLE_TASKS: true
VITE_ENABLE_SSH_KEYS: true

steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -69,6 +73,8 @@ jobs:

env:
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers
VITE_ENABLE_TASKS: true
VITE_ENABLE_SSH_KEYS: true

steps:
- name: Checkout
Expand Down
19 changes: 19 additions & 0 deletions docs/Development.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ file_share_mounts:

Instead of using the `file_share_mounts` setting, you can configure file share paths in the database. This is useful for production deployments where you want centralized management of file share paths. To use the paths in the database, set `file_share_mounts: []`. See [fileglancer-janelia](https://github.com/JaneliaSciComp/fileglancer-janelia) for an example of populating the file share paths in the database, using a private wiki source.

### Feature Flags

Optional features can be enabled via Vite environment variables. Create or edit `frontend/.env`:

```bash
# Enable background tasks/jobs feature
VITE_ENABLE_TASKS=true

# Enable SSH key management feature
VITE_ENABLE_SSH_KEYS=true
```

After changing `.env`, rebuild the frontend and restart the dev server:

```bash
pixi run node-build
pixi run dev-launch
```

### Running with SSL/HTTPS (Secure Mode)

By default, `pixi run dev-launch` runs the server in insecure HTTP mode on port 7878. This is suitable for most local development scenarios.
Expand Down
110 changes: 110 additions & 0 deletions fileglancer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from fileglancer.user_context import UserContext, EffectiveUserContext, CurrentUserContext, UserContextConfigurationError
from fileglancer.filestore import Filestore
from fileglancer.log import AccessLogMiddleware
from fileglancer import sshkeys

from x2s3.utils import get_read_access_acl, get_nosuchbucket_response, get_error_response
from x2s3.client_file import FileProxyClient
Expand Down Expand Up @@ -862,6 +863,115 @@ async def get_profile(username: str = Depends(get_current_user)):
"groups": user_groups,
}

# SSH Key Management endpoints
@app.get("/api/ssh-keys", response_model=sshkeys.SSHKeyListResponse,
description="List all SSH keys in the user's ~/.ssh directory")
async def list_ssh_keys(username: str = Depends(get_current_user)):
"""List SSH keys for the authenticated user"""
with _get_user_context(username):
try:
ssh_dir = sshkeys.get_ssh_directory()
keys = sshkeys.list_ssh_keys(ssh_dir)
return sshkeys.SSHKeyListResponse(keys=keys)
except Exception as e:
logger.error(f"Error listing SSH keys for {username}: {e}")
raise HTTPException(status_code=500, detail=str(e))

@app.post("/api/ssh-keys", response_model=sshkeys.GenerateKeyResponse,
description="Generate the default ed25519 SSH key (id_ed25519)")
async def generate_ssh_key(
request: sshkeys.GenerateKeyRequest = Body(default=sshkeys.GenerateKeyRequest()),
username: str = Depends(get_current_user)
):
"""Generate the default SSH key (id_ed25519) and add it to authorized_keys"""
with _get_user_context(username):
try:
ssh_dir = sshkeys.get_ssh_directory()
key_info = sshkeys.generate_ssh_key(ssh_dir, request.passphrase)

# Read public key content to add to authorized_keys
pubkey_path = os.path.join(ssh_dir, f"{key_info.filename}.pub")
with open(pubkey_path, 'r') as f:
public_key = f.read().strip()

# Add to authorized_keys
sshkeys.add_to_authorized_keys(ssh_dir, public_key)

# Update the is_authorized flag
key_info = sshkeys.SSHKeyInfo(
filename=key_info.filename,
key_type=key_info.key_type,
fingerprint=key_info.fingerprint,
comment=key_info.comment,
has_private_key=key_info.has_private_key,
is_authorized=True
)

return sshkeys.GenerateKeyResponse(
key=key_info,
message="SSH key generated and added to authorized_keys"
)

except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e:
raise HTTPException(status_code=500, detail=str(e))
except Exception as e:
logger.error(f"Error generating SSH key for {username}: {e}")
raise HTTPException(status_code=500, detail=str(e))

@app.post("/api/ssh-keys/authorize",
description="Add the default SSH key to authorized_keys")
async def authorize_ssh_key(username: str = Depends(get_current_user)):
"""Add the id_ed25519 key to authorized_keys"""
with _get_user_context(username):
try:
ssh_dir = sshkeys.get_ssh_directory()
pubkey_path = os.path.join(ssh_dir, "id_ed25519.pub")

if not os.path.exists(pubkey_path):
raise HTTPException(status_code=404, detail="SSH key not found")

# Read the public key
with open(pubkey_path, 'r') as f:
public_key = f.read().strip()

# Add to authorized_keys
sshkeys.add_to_authorized_keys(ssh_dir, public_key)

return {"message": "Key added to authorized_keys"}

except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e:
raise HTTPException(status_code=500, detail=str(e))
except Exception as e:
logger.error(f"Error authorizing SSH key for {username}: {e}")
raise HTTPException(status_code=500, detail=str(e))

@app.get("/api/ssh-keys/content", response_model=sshkeys.SSHKeyContent,
description="Get the content of the default SSH key (id_ed25519)")
async def get_ssh_key_content(
key_type: str = Query(..., description="Type of key to fetch: 'public' or 'private'"),
username: str = Depends(get_current_user)
):
"""Get the public or private key content for copying"""
if key_type not in ("public", "private"):
raise HTTPException(status_code=400, detail="key_type must be 'public' or 'private'")

with _get_user_context(username):
try:
ssh_dir = sshkeys.get_ssh_directory()
return sshkeys.get_key_content(ssh_dir, "id_ed25519", key_type)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shred the key contents in a bytearray after usage. Ensure the contents get shreded even if an error occurs. Consider using a context manager here.


except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Error getting SSH key content for {username}: {e}")
raise HTTPException(status_code=500, detail=str(e))

# File content endpoint
@app.head("/api/content/{path_name:path}")
async def head_file_content(path_name: str,
Expand Down
Loading