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
116 changes: 116 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
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 @@ -841,6 +842,121 @@ 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 a new ed25519 SSH key")
async def generate_ssh_key(
body: sshkeys.GenerateKeyRequest,
username: str = Depends(get_current_user)
):
"""Generate a new SSH key for the authenticated user"""
with _get_user_context(username):
try:
ssh_dir = sshkeys.get_ssh_directory()
key_info = sshkeys.generate_ssh_key(
ssh_dir,
body.key_name,
body.comment
)

message = f"SSH key '{body.key_name}' generated successfully"

# Optionally add to authorized_keys
if body.add_to_authorized_keys:
sshkeys.add_to_authorized_keys(ssh_dir, key_info.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,
public_key=key_info.public_key,
has_private_key=key_info.has_private_key,
is_authorized=True
)
message += " and added to authorized_keys"

return sshkeys.GenerateKeyResponse(key=key_info, message=message)

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/{key_name}/authorize",
description="Add a public key to authorized_keys")
async def authorize_ssh_key(
key_name: str = Path(..., description="The name of the key file (without extension)"),
username: str = Depends(get_current_user)
):
"""Add a public key to authorized_keys for cluster SSH access"""
with _get_user_context(username):
try:
# Validate key name
sshkeys.validate_key_name(key_name)

ssh_dir = sshkeys.get_ssh_directory()
# Use safe_join_path to prevent path traversal
pubkey_path = sshkeys.safe_join_path(ssh_dir, f"{key_name}.pub")

if not os.path.exists(pubkey_path):
raise HTTPException(status_code=404, detail=f"Public key '{key_name}.pub' 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": f"Key '{key_name}' added to authorized_keys"}

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

@app.delete("/api/ssh-keys/{key_name}",
description="Delete an SSH key pair")
async def delete_ssh_key(
key_name: str = Path(..., description="The name of the key file (without extension)"),
username: str = Depends(get_current_user)
):
"""Delete an SSH key pair (both private and public key files)"""
with _get_user_context(username):
try:
ssh_dir = sshkeys.get_ssh_directory()
sshkeys.delete_ssh_key(ssh_dir, key_name)
return {"message": f"Key '{key_name}' deleted successfully"}

except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e:
logger.error(f"Error deleting SSH key for {username}: {e}")
raise HTTPException(status_code=500, detail=str(e))
except Exception as e:
logger.error(f"Unexpected error deleting SSH key 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