diff --git a/backend/app/routes.py b/backend/app/routes.py index ddff35a..2c8eee4 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -1795,14 +1795,63 @@ def list_project_permissions( ): """ List all access permissions for a project. + Includes both explicit permissions and team-based access. Requires admin access to the project. """ project = check_project_access(db, project_name, current_user, "admin") auth_service = AuthorizationService(db) - permissions = auth_service.list_project_permissions(str(project.id)) + explicit_permissions = auth_service.list_project_permissions(str(project.id)) - return permissions + # Convert to response format with source field + result = [] + for perm in explicit_permissions: + result.append(AccessPermissionResponse( + id=perm.id, + project_id=perm.project_id, + user_id=perm.user_id, + level=perm.level, + created_at=perm.created_at, + expires_at=perm.expires_at, + source="explicit", + )) + + # Add team-based access if project belongs to a team + if project.team_id: + team = db.query(Team).filter(Team.id == project.team_id).first() + if team: + memberships = ( + db.query(TeamMembership) + .join(User, TeamMembership.user_id == User.id) + .filter(TeamMembership.team_id == project.team_id) + .all() + ) + + # Track users who already have explicit permissions + explicit_users = {p.user_id for p in result} + + for membership in memberships: + user = db.query(User).filter(User.id == membership.user_id).first() + if user and user.username not in explicit_users: + # Map team role to project access level + if membership.role in ("owner", "admin"): + level = "admin" + else: + level = "read" + + result.append(AccessPermissionResponse( + id=membership.id, # Use membership ID + project_id=project.id, + user_id=user.username, + level=level, + created_at=membership.created_at, + expires_at=None, + source="team", + team_slug=team.slug, + team_role=membership.role, + )) + + return result @router.post( diff --git a/backend/app/schemas.py b/backend/app/schemas.py index c6caf87..d378a8c 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -911,6 +911,9 @@ class AccessPermissionResponse(BaseModel): level: str created_at: datetime expires_at: Optional[datetime] + source: Optional[str] = "explicit" # "explicit" or "team" + team_slug: Optional[str] = None # Team slug if source is "team" + team_role: Optional[str] = None # Team role if source is "team" class Config: from_attributes = True diff --git a/frontend/src/components/AccessManagement.css b/frontend/src/components/AccessManagement.css index 21c8d5d..3fdf3c0 100644 --- a/frontend/src/components/AccessManagement.css +++ b/frontend/src/components/AccessManagement.css @@ -114,3 +114,32 @@ font-size: 0.875rem; color: var(--text-primary); } + +/* Access source styling */ +.access-source { + display: inline-block; + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; +} + +.access-source--explicit { + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +.access-source--team { + background: var(--color-info-bg, #e3f2fd); + color: var(--color-info, #1976d2); +} + +/* Team access row styling */ +.team-access-row { + background: var(--bg-secondary, #fafafa); +} + +.team-access-row td.actions .text-muted { + font-size: 0.8125rem; + font-style: italic; +} diff --git a/frontend/src/components/AccessManagement.tsx b/frontend/src/components/AccessManagement.tsx index 6201661..bb903a9 100644 --- a/frontend/src/components/AccessManagement.tsx +++ b/frontend/src/components/AccessManagement.tsx @@ -208,85 +208,104 @@ export function AccessManagement({ projectName }: AccessManagementProps) { User Access Level + Source Granted Expires Actions - {permissions.map((p) => ( - - {p.user_id} - - {editingUser === p.user_id ? ( - - ) : ( - - {p.level} - - )} - - {new Date(p.created_at).toLocaleDateString()} - - {editingUser === p.user_id ? ( - setEditExpiresAt(e.target.value)} - disabled={submitting} - min={new Date().toISOString().split('T')[0]} - /> - ) : ( - formatExpiration(p.expires_at) - )} - - - {editingUser === p.user_id ? ( - <> - - - ) : ( - <> - - - - )} - - - ))} + min={new Date().toISOString().split('T')[0]} + /> + ) : ( + formatExpiration(p.expires_at) + )} + + + {isTeamBased ? ( + + Via team + + ) : editingUser === p.user_id ? ( + <> + + + + ) : ( + <> + + + + )} + + + ); + })} )} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 099737f..eb9306c 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -320,6 +320,8 @@ export interface UserUpdate { } // Access Permission types +export type AccessSource = 'explicit' | 'team'; + export interface AccessPermission { id: string; project_id: string; @@ -327,6 +329,9 @@ export interface AccessPermission { level: AccessLevel; created_at: string; expires_at: string | null; + source?: AccessSource; // "explicit" or "team" + team_slug?: string; // Team slug if source is "team" + team_role?: string; // Team role if source is "team" } export interface AccessPermissionCreate {