Feat(Fix): Key Timezone, Expiry, Depart Link
This commit is contained in:
+279
-195
@@ -16,6 +16,7 @@ from gatehouse_app.exceptions import (
|
||||
from gatehouse_app.utils.constants import AuditAction
|
||||
from gatehouse_app.models import AuditLog
|
||||
from gatehouse_app.utils.decorators import login_required
|
||||
from gatehouse_app.utils.response import api_response
|
||||
|
||||
ssh_bp = Blueprint('ssh', __name__, url_prefix='/ssh')
|
||||
ssh_key_service = SSHKeyService()
|
||||
@@ -112,7 +113,7 @@ def _get_or_create_system_ca():
|
||||
return None
|
||||
|
||||
|
||||
def _persist_certificate(user_id, ssh_key_id, ca, signing_response, request_ip=None):
|
||||
def _persist_certificate(user_id, ssh_key_id, ca, signing_response, request_ip=None, cert_type_str='user'):
|
||||
"""Save a signed certificate to the ssh_certificates table.
|
||||
|
||||
Args:
|
||||
@@ -121,6 +122,7 @@ def _persist_certificate(user_id, ssh_key_id, ca, signing_response, request_ip=N
|
||||
ca: CA model instance (may be None — cert still returned but not persisted)
|
||||
signing_response: SSHCertificateSigningResponse
|
||||
request_ip: Client IP address
|
||||
cert_type_str: 'user' or 'host' (from the sign request)
|
||||
|
||||
Returns:
|
||||
SSHCertificate instance or None if persistence failed
|
||||
@@ -133,6 +135,11 @@ def _persist_certificate(user_id, ssh_key_id, ca, signing_response, request_ip=N
|
||||
from gatehouse_app.models.ssh_certificate import SSHCertificate, CertificateStatus
|
||||
from gatehouse_app.models.ca import CertType
|
||||
|
||||
try:
|
||||
resolved_cert_type = CertType(cert_type_str)
|
||||
except ValueError:
|
||||
resolved_cert_type = CertType.USER
|
||||
|
||||
cert_record = SSHCertificate(
|
||||
ca_id=ca.id,
|
||||
user_id=user_id,
|
||||
@@ -140,7 +147,7 @@ def _persist_certificate(user_id, ssh_key_id, ca, signing_response, request_ip=N
|
||||
certificate=signing_response.certificate,
|
||||
serial=signing_response.serial,
|
||||
key_id=str(ssh_key_id),
|
||||
cert_type=CertType.USER,
|
||||
cert_type=resolved_cert_type,
|
||||
principals=signing_response.principals,
|
||||
valid_after=signing_response.valid_after,
|
||||
valid_before=signing_response.valid_before,
|
||||
@@ -172,10 +179,13 @@ def list_ssh_keys():
|
||||
user_id = g.current_user.id
|
||||
|
||||
keys = ssh_key_service.get_user_ssh_keys(user_id)
|
||||
return jsonify({
|
||||
'keys': [k.to_dict() for k in keys],
|
||||
'count': len(keys),
|
||||
}), 200
|
||||
return api_response(
|
||||
data={
|
||||
'keys': [k.to_dict() for k in keys],
|
||||
'count': len(keys),
|
||||
},
|
||||
message="SSH keys retrieved successfully"
|
||||
)
|
||||
|
||||
|
||||
@ssh_bp.route('/keys', methods=['POST'])
|
||||
@@ -183,25 +193,24 @@ def list_ssh_keys():
|
||||
def add_ssh_key():
|
||||
"""Add a new SSH public key for current user."""
|
||||
user_id = g.current_user.id
|
||||
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': 'No JSON data provided'}), 400
|
||||
|
||||
return api_response(success=False, message='No JSON data provided', status=400, error_type='BAD_REQUEST')
|
||||
|
||||
public_key = data.get('public_key') or data.get('key')
|
||||
description = data.get('description')
|
||||
|
||||
|
||||
if not public_key:
|
||||
return jsonify({'error': 'public_key is required'}), 400
|
||||
|
||||
return api_response(success=False, message='public_key is required', status=400, error_type='BAD_REQUEST')
|
||||
|
||||
try:
|
||||
ssh_key = ssh_key_service.add_ssh_key(
|
||||
user_id=user_id,
|
||||
public_key=public_key,
|
||||
description=description,
|
||||
)
|
||||
|
||||
# Audit log
|
||||
|
||||
AuditLog.log(
|
||||
action=AuditAction.SSH_KEY_ADDED,
|
||||
user_id=user_id,
|
||||
@@ -209,17 +218,17 @@ def add_ssh_key():
|
||||
resource_id=ssh_key.id,
|
||||
ip_address=request.remote_addr,
|
||||
)
|
||||
|
||||
return jsonify(ssh_key.to_dict()), 201
|
||||
|
||||
|
||||
return api_response(success=True, message='SSH key added', data=ssh_key.to_dict(), status=201)
|
||||
|
||||
except SSHKeyAlreadyExistsError as e:
|
||||
return jsonify({'error': e.message, 'code': 'SSH_KEY_ALREADY_EXISTS'}), 409
|
||||
return api_response(success=False, message=e.message, status=409, error_type='SSH_KEY_ALREADY_EXISTS')
|
||||
except IntegrityError:
|
||||
return jsonify({'error': 'SSH key already exists', 'code': 'SSH_KEY_ALREADY_EXISTS'}), 409
|
||||
return api_response(success=False, message='SSH key already exists', status=409, error_type='SSH_KEY_ALREADY_EXISTS')
|
||||
except SSHKeyError as e:
|
||||
return jsonify({'error': str(e)}), 400
|
||||
return api_response(success=False, message=str(e), status=400, error_type='SSH_KEY_ERROR')
|
||||
except ValidationError as e:
|
||||
return jsonify({'error': str(e)}), 400
|
||||
return api_response(success=False, message=str(e), status=400, error_type='VALIDATION_ERROR')
|
||||
|
||||
|
||||
@ssh_bp.route('/keys/<key_id>', methods=['GET'])
|
||||
@@ -227,18 +236,17 @@ def add_ssh_key():
|
||||
def get_ssh_key(key_id):
|
||||
"""Get a specific SSH key."""
|
||||
user_id = g.current_user.id
|
||||
|
||||
|
||||
try:
|
||||
ssh_key = ssh_key_service.get_ssh_key(key_id)
|
||||
|
||||
# Check ownership
|
||||
|
||||
if ssh_key.user_id != user_id:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
return jsonify(ssh_key.to_dict()), 200
|
||||
|
||||
return api_response(success=False, message='Forbidden', status=403, error_type='FORBIDDEN')
|
||||
|
||||
return api_response(success=True, message='SSH key retrieved', data=ssh_key.to_dict(), status=200)
|
||||
|
||||
except SSHKeyNotFoundError:
|
||||
return jsonify({'error': 'SSH key not found'}), 404
|
||||
return api_response(success=False, message='SSH key not found', status=404, error_type='NOT_FOUND')
|
||||
|
||||
|
||||
@ssh_bp.route('/keys/<key_id>', methods=['DELETE'])
|
||||
@@ -246,17 +254,15 @@ def get_ssh_key(key_id):
|
||||
def delete_ssh_key(key_id):
|
||||
"""Delete an SSH key."""
|
||||
user_id = g.current_user.id
|
||||
|
||||
|
||||
try:
|
||||
ssh_key = ssh_key_service.get_ssh_key(key_id)
|
||||
|
||||
# Check ownership
|
||||
|
||||
if ssh_key.user_id != user_id:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
return api_response(success=False, message='Forbidden', status=403, error_type='FORBIDDEN')
|
||||
|
||||
ssh_key_service.delete_ssh_key(key_id)
|
||||
|
||||
# Audit log
|
||||
|
||||
AuditLog.log(
|
||||
action=AuditAction.SSH_KEY_DELETED,
|
||||
user_id=user_id,
|
||||
@@ -264,11 +270,11 @@ def delete_ssh_key(key_id):
|
||||
resource_id=key_id,
|
||||
ip_address=request.remote_addr,
|
||||
)
|
||||
|
||||
return jsonify({'status': 'deleted'}), 200
|
||||
|
||||
|
||||
return api_response(success=True, message='SSH key deleted', data={'status': 'deleted'}, status=200)
|
||||
|
||||
except SSHKeyNotFoundError:
|
||||
return jsonify({'error': 'SSH key not found'}), 404
|
||||
return api_response(success=False, message='SSH key not found', status=404, error_type='NOT_FOUND')
|
||||
|
||||
|
||||
@ssh_bp.route('/keys/<key_id>/verify', methods=['GET', 'POST'])
|
||||
@@ -276,37 +282,34 @@ def delete_ssh_key(key_id):
|
||||
def verify_ssh_key(key_id):
|
||||
"""Generate or verify SSH key ownership challenge."""
|
||||
user_id = g.current_user.id
|
||||
|
||||
|
||||
try:
|
||||
ssh_key = ssh_key_service.get_ssh_key(key_id)
|
||||
|
||||
# Check ownership
|
||||
|
||||
if ssh_key.user_id != user_id:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
# Handle GET request - return challenge
|
||||
return api_response(success=False, message='Forbidden', status=403, error_type='FORBIDDEN')
|
||||
|
||||
# GET — return a fresh challenge
|
||||
if request.method == 'GET':
|
||||
challenge = ssh_key_service.generate_verification_challenge(key_id)
|
||||
return jsonify({
|
||||
return api_response(success=True, message='Challenge generated', data={
|
||||
'challenge_text': challenge,
|
||||
'validationText': challenge, # Backwards compatibility
|
||||
'validationText': challenge,
|
||||
'key_id': key_id,
|
||||
}), 200
|
||||
|
||||
# Handle POST request - verify signature
|
||||
}, status=200)
|
||||
|
||||
# POST — verify signature or generate challenge
|
||||
data = request.get_json() or {}
|
||||
action = data.get('action', 'verify_signature')
|
||||
|
||||
|
||||
if action == 'verify_signature':
|
||||
# Verify signature
|
||||
signature = data.get('signature')
|
||||
if not signature:
|
||||
return jsonify({'error': 'signature is required'}), 400
|
||||
|
||||
return api_response(success=False, message='signature is required', status=400, error_type='BAD_REQUEST')
|
||||
|
||||
try:
|
||||
verified = ssh_key_service.verify_ssh_key_ownership(key_id, signature)
|
||||
|
||||
# Audit log
|
||||
|
||||
AuditLog.log(
|
||||
action=AuditAction.SSH_KEY_VERIFIED,
|
||||
user_id=user_id,
|
||||
@@ -315,9 +318,9 @@ def verify_ssh_key(key_id):
|
||||
ip_address=request.remote_addr,
|
||||
success=verified,
|
||||
)
|
||||
|
||||
return jsonify({'verified': verified}), 200
|
||||
|
||||
|
||||
return api_response(success=True, message='Verification complete', data={'verified': verified}, status=200)
|
||||
|
||||
except Exception as e:
|
||||
AuditLog.log(
|
||||
action=AuditAction.SSH_KEY_VALIDATION_FAILED,
|
||||
@@ -328,18 +331,17 @@ def verify_ssh_key(key_id):
|
||||
success=False,
|
||||
error_message=str(e),
|
||||
)
|
||||
return jsonify({'error': str(e)}), 400
|
||||
|
||||
return api_response(success=False, message=str(e), status=400, error_type='VERIFICATION_FAILED')
|
||||
|
||||
else: # generate_challenge
|
||||
# Generate verification challenge
|
||||
challenge = ssh_key_service.generate_verification_challenge(key_id)
|
||||
return jsonify({
|
||||
return api_response(success=True, message='Challenge generated', data={
|
||||
'challenge_text': challenge,
|
||||
'challenge': challenge, # Both for compatibility
|
||||
}), 200
|
||||
|
||||
'challenge': challenge,
|
||||
}, status=200)
|
||||
|
||||
except SSHKeyNotFoundError:
|
||||
return jsonify({'error': 'SSH key not found'}), 404
|
||||
return api_response(success=False, message='SSH key not found', status=404, error_type='NOT_FOUND')
|
||||
|
||||
|
||||
@ssh_bp.route('/keys/<key_id>/update-description', methods=['PATCH'])
|
||||
@@ -347,27 +349,23 @@ def verify_ssh_key(key_id):
|
||||
def update_ssh_key_description(key_id):
|
||||
"""Update SSH key description."""
|
||||
user_id = g.current_user.id
|
||||
|
||||
|
||||
data = request.get_json()
|
||||
if not data or 'description' not in data:
|
||||
return jsonify({'error': 'description is required'}), 400
|
||||
|
||||
return api_response(success=False, message='description is required', status=400, error_type='BAD_REQUEST')
|
||||
|
||||
try:
|
||||
ssh_key = ssh_key_service.get_ssh_key(key_id)
|
||||
|
||||
# Check ownership
|
||||
|
||||
if ssh_key.user_id != user_id:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
updated_key = ssh_key_service.update_ssh_key_description(
|
||||
key_id,
|
||||
data['description']
|
||||
)
|
||||
|
||||
return jsonify(updated_key.to_dict()), 200
|
||||
|
||||
return api_response(success=False, message='Forbidden', status=403, error_type='FORBIDDEN')
|
||||
|
||||
updated_key = ssh_key_service.update_ssh_key_description(key_id, data['description'])
|
||||
|
||||
return api_response(success=True, message='Description updated', data=updated_key.to_dict(), status=200)
|
||||
|
||||
except SSHKeyNotFoundError:
|
||||
return jsonify({'error': 'SSH key not found'}), 404
|
||||
return api_response(success=False, message='SSH key not found', status=404, error_type='NOT_FOUND')
|
||||
|
||||
|
||||
@ssh_bp.route('/sign', methods=['POST'])
|
||||
@@ -376,92 +374,119 @@ def sign_certificate():
|
||||
"""Sign an SSH certificate for the current user."""
|
||||
user = g.current_user
|
||||
user_id = user.id
|
||||
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': 'No JSON data provided'}), 400
|
||||
|
||||
try:
|
||||
principals = data.get('principals', [])
|
||||
cert_type = data.get('cert_type', 'user')
|
||||
# Accept both 'key_id' and 'cert_id' (from CLI)
|
||||
key_id = data.get('key_id') or data.get('cert_id')
|
||||
expiry_hours = data.get('expiry_hours')
|
||||
|
||||
return api_response(success=False, message="No JSON data provided", status=400, error_type="BAD_REQUEST")
|
||||
|
||||
requested_principals = data.get('principals') or []
|
||||
cert_type = data.get('cert_type', 'user')
|
||||
key_id = data.get('key_id') or data.get('cert_id')
|
||||
expiry_hours = data.get('expiry_hours')
|
||||
|
||||
# ── Resolve which principals the user is allowed to use ──────────────────
|
||||
from gatehouse_app.models.organization_member import OrganizationMember
|
||||
from gatehouse_app.models.principal import Principal, PrincipalMembership
|
||||
from gatehouse_app.models.department import DepartmentMembership, DepartmentPrincipal
|
||||
from gatehouse_app.utils.constants import OrganizationRole
|
||||
|
||||
allowed_principal_names = set()
|
||||
|
||||
memberships = OrganizationMember.query.filter_by(user_id=user_id).all()
|
||||
for om in memberships:
|
||||
org = om.organization
|
||||
if not org or org.deleted_at is not None:
|
||||
continue
|
||||
role = om.role
|
||||
if role in (OrganizationRole.ADMIN, OrganizationRole.OWNER):
|
||||
# Admin/owner can use any principal in the org
|
||||
for p in Principal.query.filter_by(organization_id=org.id, deleted_at=None).all():
|
||||
allowed_principal_names.add(p.name)
|
||||
else:
|
||||
# Direct memberships
|
||||
for pm in PrincipalMembership.query.filter_by(user_id=user_id, deleted_at=None).all():
|
||||
if pm.principal and pm.principal.organization_id == org.id and pm.principal.deleted_at is None:
|
||||
allowed_principal_names.add(pm.principal.name)
|
||||
# Via department
|
||||
for dm in DepartmentMembership.query.filter_by(user_id=user_id, deleted_at=None).all():
|
||||
if dm.department and dm.department.organization_id == org.id and dm.department.deleted_at is None:
|
||||
for dp in DepartmentPrincipal.query.filter_by(department_id=dm.department_id, deleted_at=None).all():
|
||||
if dp.principal and dp.principal.deleted_at is None:
|
||||
allowed_principal_names.add(dp.principal.name)
|
||||
|
||||
# ── Determine final principals list ─────────────────────────────────────
|
||||
if not requested_principals:
|
||||
# Auto-resolve: use all principals the user is assigned to
|
||||
principals = list(allowed_principal_names)
|
||||
if not principals:
|
||||
return jsonify({'error': 'principals is required'}), 400
|
||||
|
||||
# If key_id not specified, use first verified key
|
||||
if not key_id:
|
||||
verified_keys = ssh_key_service.get_user_verified_ssh_keys(user_id)
|
||||
if not verified_keys:
|
||||
return jsonify({'error': 'No verified SSH keys found'}), 400
|
||||
key_id = verified_keys[0].id
|
||||
|
||||
# Get the SSH key
|
||||
return api_response(
|
||||
success=False,
|
||||
message="You have no principals assigned. Ask an admin to add you to a principal.",
|
||||
status=400,
|
||||
error_type="NO_PRINCIPALS",
|
||||
)
|
||||
else:
|
||||
# Validate each requested principal is within the user's allowed set
|
||||
invalid = [p for p in requested_principals if p not in allowed_principal_names]
|
||||
if invalid:
|
||||
return api_response(
|
||||
success=False,
|
||||
message=f"You are not authorised to request principals: {', '.join(invalid)}",
|
||||
status=403,
|
||||
error_type="UNAUTHORIZED_PRINCIPALS",
|
||||
)
|
||||
principals = requested_principals
|
||||
|
||||
# ── Key resolution ────────────────────────────────────────────────────────
|
||||
if not key_id:
|
||||
verified_keys = ssh_key_service.get_user_verified_ssh_keys(user_id)
|
||||
if not verified_keys:
|
||||
return api_response(
|
||||
success=False,
|
||||
message="No verified SSH keys found. Verify a key before requesting a certificate.",
|
||||
status=400,
|
||||
error_type="NO_VERIFIED_KEYS",
|
||||
)
|
||||
key_id = verified_keys[0].id
|
||||
|
||||
try:
|
||||
ssh_key = ssh_key_service.get_ssh_key(key_id)
|
||||
if ssh_key.user_id != user_id:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
|
||||
if not ssh_key.verified:
|
||||
return jsonify({'error': 'SSH key is not verified'}), 400
|
||||
|
||||
# Resolve which CA to use: org DB CA > config-file CA
|
||||
db_ca = _get_org_ca_for_user(user)
|
||||
ca_private_key = db_ca.private_key if db_ca else None # None → signing service uses config
|
||||
|
||||
# Create signing request
|
||||
signing_request = SSHCertificateSigningRequest(
|
||||
ssh_public_key=ssh_key.payload,
|
||||
principals=principals,
|
||||
cert_type=cert_type,
|
||||
key_id=key_id,
|
||||
expiry_hours=int(expiry_hours) if expiry_hours else None,
|
||||
)
|
||||
|
||||
# Validate request
|
||||
validation_errors = signing_request.validate()
|
||||
if validation_errors:
|
||||
return jsonify({'errors': validation_errors}), 400
|
||||
|
||||
# Sign the certificate (pass ca_private_key=None → service loads from config)
|
||||
response = ssh_ca_service.sign_certificate(signing_request, ca_private_key=ca_private_key)
|
||||
|
||||
# Persist certificate to DB
|
||||
# If user's org has no DB CA, use the system-config-ca record
|
||||
ca_for_db = db_ca or _get_or_create_system_ca()
|
||||
cert_record = _persist_certificate(
|
||||
user_id=user_id,
|
||||
ssh_key_id=key_id,
|
||||
ca=ca_for_db,
|
||||
signing_response=response,
|
||||
request_ip=request.remote_addr,
|
||||
)
|
||||
|
||||
# Audit log
|
||||
AuditLog.log(
|
||||
action=AuditAction.SSH_CERT_ISSUED,
|
||||
user_id=user_id,
|
||||
resource_type='SSHCertificate',
|
||||
resource_id=cert_record.id if cert_record else key_id,
|
||||
ip_address=request.remote_addr,
|
||||
description=f'Certificate issued for principals: {", ".join(principals)}',
|
||||
)
|
||||
|
||||
result = {
|
||||
'certificate': response.certificate,
|
||||
'serial': response.serial,
|
||||
'principals': response.principals,
|
||||
'valid_after': response.valid_after.isoformat() if response.valid_after else None,
|
||||
'valid_before': response.valid_before.isoformat() if response.valid_before else None,
|
||||
}
|
||||
if cert_record:
|
||||
result['cert_id'] = str(cert_record.id)
|
||||
|
||||
return jsonify(result), 201
|
||||
|
||||
except SSHKeyNotFoundError:
|
||||
return jsonify({'error': 'SSH key not found'}), 404
|
||||
return api_response(success=False, message="SSH key not found", status=404, error_type="NOT_FOUND")
|
||||
|
||||
if ssh_key.user_id != user_id:
|
||||
return api_response(success=False, message="Forbidden", status=403, error_type="FORBIDDEN")
|
||||
|
||||
if not ssh_key.verified:
|
||||
return api_response(
|
||||
success=False,
|
||||
message="SSH key is not verified. Verify it before requesting a certificate.",
|
||||
status=400,
|
||||
error_type="KEY_NOT_VERIFIED",
|
||||
)
|
||||
|
||||
db_ca = _get_org_ca_for_user(user)
|
||||
ca_private_key = db_ca.private_key if db_ca else None
|
||||
|
||||
signing_request = SSHCertificateSigningRequest(
|
||||
ssh_public_key=ssh_key.payload,
|
||||
principals=principals,
|
||||
cert_type=cert_type,
|
||||
key_id=key_id,
|
||||
expiry_hours=int(expiry_hours) if expiry_hours else None,
|
||||
)
|
||||
validation_errors = signing_request.validate()
|
||||
if validation_errors:
|
||||
return api_response(
|
||||
success=False,
|
||||
message="Invalid signing request",
|
||||
status=400,
|
||||
error_type="VALIDATION_ERROR",
|
||||
error_details={"errors": validation_errors},
|
||||
)
|
||||
|
||||
try:
|
||||
response = ssh_ca_service.sign_certificate(signing_request, ca_private_key=ca_private_key)
|
||||
except SSHCertificateError as e:
|
||||
AuditLog.log(
|
||||
action=AuditAction.SSH_CERT_FAILED,
|
||||
@@ -471,7 +496,7 @@ def sign_certificate():
|
||||
success=False,
|
||||
error_message=str(e),
|
||||
)
|
||||
return jsonify({'error': str(e)}), 400
|
||||
return api_response(success=False, message=str(e), status=400, error_type="SIGNING_FAILED")
|
||||
except Exception as e:
|
||||
AuditLog.log(
|
||||
action=AuditAction.SSH_CERT_FAILED,
|
||||
@@ -481,7 +506,38 @@ def sign_certificate():
|
||||
success=False,
|
||||
error_message=str(e),
|
||||
)
|
||||
return jsonify({'error': 'Certificate signing failed: ' + str(e)}), 500
|
||||
return api_response(success=False, message="Certificate signing failed", status=500, error_type="SERVER_ERROR")
|
||||
|
||||
ca_for_db = db_ca or _get_or_create_system_ca()
|
||||
cert_record = _persist_certificate(
|
||||
user_id=user_id,
|
||||
ssh_key_id=key_id,
|
||||
ca=ca_for_db,
|
||||
signing_response=response,
|
||||
request_ip=request.remote_addr,
|
||||
cert_type_str=cert_type,
|
||||
)
|
||||
|
||||
AuditLog.log(
|
||||
action=AuditAction.SSH_CERT_ISSUED,
|
||||
user_id=user_id,
|
||||
resource_type='SSHCertificate',
|
||||
resource_id=cert_record.id if cert_record else key_id,
|
||||
ip_address=request.remote_addr,
|
||||
description=f'Certificate issued for principals: {", ".join(principals)}',
|
||||
)
|
||||
|
||||
result = {
|
||||
'certificate': response.certificate,
|
||||
'serial': response.serial,
|
||||
'principals': response.principals,
|
||||
'valid_after': response.valid_after.isoformat() if response.valid_after else None,
|
||||
'valid_before': response.valid_before.isoformat() if response.valid_before else None,
|
||||
}
|
||||
if cert_record:
|
||||
result['cert_id'] = str(cert_record.id)
|
||||
|
||||
return api_response(data=result, message="Certificate signed successfully", status=201)
|
||||
|
||||
|
||||
@ssh_bp.route('/certificates', methods=['GET'])
|
||||
@@ -498,12 +554,20 @@ def list_certificates():
|
||||
.order_by(SSHCertificate.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
return jsonify({
|
||||
'certificates': [c.to_dict() for c in certs],
|
||||
'count': len(certs),
|
||||
}), 200
|
||||
return api_response(
|
||||
data={
|
||||
'certificates': [c.to_dict() for c in certs],
|
||||
'count': len(certs),
|
||||
},
|
||||
message="Certificates retrieved successfully"
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
return api_response(
|
||||
success=False,
|
||||
message=str(e),
|
||||
status=500,
|
||||
error_type='INTERNAL_ERROR'
|
||||
)
|
||||
|
||||
|
||||
@ssh_bp.route('/certificates/<cert_id>', methods=['GET'])
|
||||
@@ -516,15 +580,14 @@ def get_certificate(cert_id):
|
||||
from gatehouse_app.models.ssh_certificate import SSHCertificate
|
||||
cert = SSHCertificate.query.filter_by(id=cert_id, deleted_at=None).first()
|
||||
if not cert:
|
||||
return jsonify({'error': 'Certificate not found'}), 404
|
||||
return api_response(success=False, message='Certificate not found', status=404, error_type='NOT_FOUND')
|
||||
if cert.user_id != user_id:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
# Include full certificate text in single-fetch endpoint
|
||||
return api_response(success=False, message='Forbidden', status=403, error_type='FORBIDDEN')
|
||||
data = cert.to_dict()
|
||||
data['certificate'] = cert.certificate
|
||||
return jsonify(data), 200
|
||||
return api_response(success=True, message='Certificate retrieved', data=data, status=200)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
return api_response(success=False, message=str(e), status=500, error_type='INTERNAL_ERROR')
|
||||
|
||||
|
||||
@ssh_bp.route('/certificates/<cert_id>/revoke', methods=['POST'])
|
||||
@@ -540,11 +603,11 @@ def revoke_certificate(cert_id):
|
||||
from gatehouse_app.models.ssh_certificate import SSHCertificate
|
||||
cert = SSHCertificate.query.filter_by(id=cert_id, deleted_at=None).first()
|
||||
if not cert:
|
||||
return jsonify({'error': 'Certificate not found'}), 404
|
||||
return api_response(success=False, message='Certificate not found', status=404, error_type='NOT_FOUND')
|
||||
if cert.user_id != user_id:
|
||||
return jsonify({'error': 'Forbidden'}), 403
|
||||
return api_response(success=False, message='Forbidden', status=403, error_type='FORBIDDEN')
|
||||
if cert.revoked:
|
||||
return jsonify({'error': 'Certificate is already revoked'}), 409
|
||||
return api_response(success=False, message='Certificate is already revoked', status=409, error_type='ALREADY_REVOKED')
|
||||
|
||||
cert.revoke(reason=reason)
|
||||
|
||||
@@ -557,9 +620,14 @@ def revoke_certificate(cert_id):
|
||||
description=f'Revoked: {reason}',
|
||||
)
|
||||
|
||||
return jsonify({'status': 'revoked', 'cert_id': cert_id, 'reason': reason}), 200
|
||||
return api_response(
|
||||
success=True,
|
||||
message='Certificate revoked successfully',
|
||||
data={'status': 'revoked', 'cert_id': cert_id, 'reason': reason},
|
||||
status=200,
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
return api_response(success=False, message=str(e), status=500, error_type='INTERNAL_ERROR')
|
||||
|
||||
|
||||
@ssh_bp.route('/ca/public-key', methods=['GET'])
|
||||
@@ -584,12 +652,15 @@ def get_ca_public_key():
|
||||
# Try org CA first
|
||||
db_ca = _get_org_ca_for_user(user)
|
||||
if db_ca:
|
||||
return jsonify({
|
||||
'public_key': db_ca.public_key,
|
||||
'fingerprint': db_ca.fingerprint,
|
||||
'ca_name': db_ca.name,
|
||||
'source': 'db',
|
||||
}), 200
|
||||
return api_response(
|
||||
data={
|
||||
'public_key': db_ca.public_key,
|
||||
'fingerprint': db_ca.fingerprint,
|
||||
'ca_name': db_ca.name,
|
||||
'source': 'db',
|
||||
},
|
||||
message="CA public key retrieved successfully"
|
||||
)
|
||||
|
||||
# Fall back to config-file CA
|
||||
try:
|
||||
@@ -601,15 +672,28 @@ def get_ca_public_key():
|
||||
with open(key_path) as f:
|
||||
pub_key = f.read().strip()
|
||||
from gatehouse_app.utils.crypto import compute_ssh_fingerprint
|
||||
return jsonify({
|
||||
'public_key': pub_key,
|
||||
'fingerprint': compute_ssh_fingerprint(pub_key),
|
||||
'ca_name': 'system-config-ca',
|
||||
'source': 'config',
|
||||
}), 200
|
||||
return api_response(
|
||||
data={
|
||||
'public_key': pub_key,
|
||||
'fingerprint': compute_ssh_fingerprint(pub_key),
|
||||
'ca_name': 'system-config-ca',
|
||||
'source': 'config',
|
||||
},
|
||||
message="CA public key retrieved successfully"
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Could not load CA public key: {e}'}), 500
|
||||
return api_response(
|
||||
success=False,
|
||||
message=f'Could not load CA public key: {e}',
|
||||
status=500,
|
||||
error_type='INTERNAL_ERROR'
|
||||
)
|
||||
|
||||
return jsonify({'error': 'No CA configured for this organization'}), 404
|
||||
return api_response(
|
||||
success=False,
|
||||
message='No CA configured for this organization',
|
||||
status=404,
|
||||
error_type='NOT_FOUND'
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user