feat(ssh): add multi-organization support for certificate signing
Add support for users who belong to multiple organizations to select which organization's CA should sign their SSH certificates. Changes: - CLI: Add --org-id and --list-orgs options for organization selection - API: Return MULTIPLE_ORGS_AMBIGUOUS error when org selection needed - API: Add /users/me/organizations/simple endpoint for CLI org listing - DB: Add organization_id to certificate_audit_logs for better tracking - Include organization_name in certificate response for clarity
This commit is contained in:
@@ -142,6 +142,55 @@ def get_my_organizations():
|
||||
return api_response(data={"organizations": orgs, "count": len(orgs)}, message="Organizations retrieved successfully")
|
||||
|
||||
|
||||
@api_v1_bp.route("/users/me/organizations/simple", methods=["GET"])
|
||||
@login_required
|
||||
def get_my_organizations_simple():
|
||||
"""Lightweight organization list for CLI tool.
|
||||
|
||||
Returns organizations with CA status indicators for CLI users.
|
||||
"""
|
||||
from gatehouse_app.models.organization.organization_member import OrganizationMember
|
||||
from gatehouse_app.models.ssh_ca.ca import CA, CaType
|
||||
|
||||
user = g.current_user
|
||||
memberships = OrganizationMember.query.filter_by(user_id=user.id, deleted_at=None).all()
|
||||
|
||||
orgs = []
|
||||
for membership in memberships:
|
||||
org = membership.organization
|
||||
if not org or org.deleted_at is not None:
|
||||
continue
|
||||
|
||||
# Check for active CAs
|
||||
user_ca = CA.query.filter_by(
|
||||
organization_id=org.id,
|
||||
ca_type=CaType.USER,
|
||||
is_active=True,
|
||||
deleted_at=None,
|
||||
).first()
|
||||
|
||||
host_ca = CA.query.filter_by(
|
||||
organization_id=org.id,
|
||||
ca_type=CaType.HOST,
|
||||
is_active=True,
|
||||
deleted_at=None,
|
||||
).first()
|
||||
|
||||
orgs.append({
|
||||
"id": str(org.id),
|
||||
"name": org.name,
|
||||
"slug": getattr(org, 'slug', None),
|
||||
"role": membership.role.value if hasattr(membership.role, "value") else str(membership.role),
|
||||
"has_user_ca": user_ca is not None,
|
||||
"has_host_ca": host_ca is not None,
|
||||
})
|
||||
|
||||
return api_response(
|
||||
data={"organizations": orgs, "count": len(orgs)},
|
||||
message="Organizations retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@api_v1_bp.route("/users/me/principals", methods=["GET"])
|
||||
@login_required
|
||||
@full_access_required
|
||||
@@ -182,12 +231,11 @@ def get_my_principals():
|
||||
|
||||
my_principals = []
|
||||
if effective_principal_ids:
|
||||
for p in Principal.query.filter(
|
||||
Principal.id.in_(list(effective_principal_ids)),
|
||||
Principal.deleted_at == None,
|
||||
).all():
|
||||
for p in Principal.query.filter(Principal.id.in_(list(effective_principal_ids)), Principal.deleted_at == None).all():
|
||||
my_principals.append({
|
||||
"id": p.id, "name": p.name, "description": p.description,
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"description": p.description,
|
||||
"direct": p.id in direct_principal_ids,
|
||||
})
|
||||
|
||||
@@ -197,7 +245,8 @@ def get_my_principals():
|
||||
all_principals.append({"id": p.id, "name": p.name, "description": p.description})
|
||||
|
||||
orgs_result.append({
|
||||
"org_id": org.id, "org_name": org.name,
|
||||
"org_id": org.id,
|
||||
"org_name": org.name,
|
||||
"role": role.value if hasattr(role, "value") else role,
|
||||
"is_admin": is_admin,
|
||||
"my_principals": my_principals,
|
||||
@@ -241,6 +290,7 @@ def get_my_pending_invites():
|
||||
|
||||
@api_v1_bp.route("/users/me/memberships", methods=["GET"])
|
||||
@login_required
|
||||
@full_access_required
|
||||
def get_my_memberships():
|
||||
from gatehouse_app.models.organization.organization_member import OrganizationMember
|
||||
from gatehouse_app.models.organization.department import DepartmentMembership, DepartmentPrincipal, Department
|
||||
@@ -258,15 +308,15 @@ def get_my_memberships():
|
||||
|
||||
dept_memberships = DepartmentMembership.query.filter_by(user_id=user.id, deleted_at=None).all()
|
||||
user_depts = [
|
||||
dm.department for dm in dept_memberships
|
||||
if dm.department
|
||||
and dm.department.organization_id == org.id
|
||||
and dm.department.deleted_at is None
|
||||
dm.department
|
||||
for dm in dept_memberships
|
||||
if dm.department and dm.department.organization_id == org.id and dm.department.deleted_at is None
|
||||
]
|
||||
|
||||
direct_pm = PrincipalMembership.query.filter_by(user_id=user.id, deleted_at=None).all()
|
||||
direct_principal_ids = {
|
||||
pm.principal_id for pm in direct_pm
|
||||
pm.principal_id
|
||||
for pm in direct_pm
|
||||
if pm.principal and pm.principal.organization_id == org.id and pm.principal.deleted_at is None
|
||||
}
|
||||
|
||||
@@ -279,18 +329,18 @@ def get_my_memberships():
|
||||
all_principal_ids = direct_principal_ids | via_dept_principal_ids
|
||||
principals_list = []
|
||||
if all_principal_ids:
|
||||
for p in Principal.query.filter(
|
||||
Principal.id.in_(list(all_principal_ids)),
|
||||
Principal.deleted_at == None,
|
||||
).all():
|
||||
for p in Principal.query.filter(Principal.id.in_(list(all_principal_ids)), Principal.deleted_at == None).all():
|
||||
principals_list.append({
|
||||
"id": str(p.id), "name": p.name, "description": p.description,
|
||||
"id": str(p.id),
|
||||
"name": p.name,
|
||||
"description": p.description,
|
||||
"via_department": p.id not in direct_principal_ids,
|
||||
})
|
||||
|
||||
role = membership.role
|
||||
orgs_result.append({
|
||||
"org_id": str(org.id), "org_name": org.name,
|
||||
"org_id": str(org.id),
|
||||
"org_name": org.name,
|
||||
"role": role.value if hasattr(role, "value") else role,
|
||||
"departments": [{"id": str(d.id), "name": d.name, "description": d.description} for d in user_depts],
|
||||
"principals": principals_list,
|
||||
|
||||
Reference in New Issue
Block a user