Show team-based access in project access management
- Add source, team_slug, team_role fields to AccessPermissionResponse schema - Update list_project_permissions endpoint to include team members with source="team" - Display team-based access in AccessManagement component with read-only styling - Add "Source" column to differentiate explicit vs team-based permissions - Team-based access shows "Via team" in actions column (not editable)
This commit is contained in:
@@ -1795,14 +1795,63 @@ def list_project_permissions(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
List all access permissions for a project.
|
List all access permissions for a project.
|
||||||
|
Includes both explicit permissions and team-based access.
|
||||||
Requires admin access to the project.
|
Requires admin access to the project.
|
||||||
"""
|
"""
|
||||||
project = check_project_access(db, project_name, current_user, "admin")
|
project = check_project_access(db, project_name, current_user, "admin")
|
||||||
|
|
||||||
auth_service = AuthorizationService(db)
|
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(
|
@router.post(
|
||||||
|
|||||||
@@ -911,6 +911,9 @@ class AccessPermissionResponse(BaseModel):
|
|||||||
level: str
|
level: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
expires_at: Optional[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:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -114,3 +114,32 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--text-primary);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -208,17 +208,20 @@ export function AccessManagement({ projectName }: AccessManagementProps) {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th>Access Level</th>
|
<th>Access Level</th>
|
||||||
|
<th>Source</th>
|
||||||
<th>Granted</th>
|
<th>Granted</th>
|
||||||
<th>Expires</th>
|
<th>Expires</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{permissions.map((p) => (
|
{permissions.map((p) => {
|
||||||
<tr key={p.id}>
|
const isTeamBased = p.source === 'team';
|
||||||
|
return (
|
||||||
|
<tr key={p.id} className={isTeamBased ? 'team-access-row' : ''}>
|
||||||
<td>{p.user_id}</td>
|
<td>{p.user_id}</td>
|
||||||
<td>
|
<td>
|
||||||
{editingUser === p.user_id ? (
|
{editingUser === p.user_id && !isTeamBased ? (
|
||||||
<select
|
<select
|
||||||
value={editLevel}
|
value={editLevel}
|
||||||
onChange={(e) => setEditLevel(e.target.value as AccessLevel)}
|
onChange={(e) => setEditLevel(e.target.value as AccessLevel)}
|
||||||
@@ -234,9 +237,20 @@ export function AccessManagement({ projectName }: AccessManagementProps) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{isTeamBased ? (
|
||||||
|
<span className="access-source access-source--team" title={`Team role: ${p.team_role}`}>
|
||||||
|
Team: {p.team_slug}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="access-source access-source--explicit">
|
||||||
|
Explicit
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td>{new Date(p.created_at).toLocaleDateString()}</td>
|
<td>{new Date(p.created_at).toLocaleDateString()}</td>
|
||||||
<td>
|
<td>
|
||||||
{editingUser === p.user_id ? (
|
{editingUser === p.user_id && !isTeamBased ? (
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={editExpiresAt}
|
value={editExpiresAt}
|
||||||
@@ -249,7 +263,11 @@ export function AccessManagement({ projectName }: AccessManagementProps) {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="actions">
|
<td className="actions">
|
||||||
{editingUser === p.user_id ? (
|
{isTeamBased ? (
|
||||||
|
<span className="text-muted" title="Manage access via team settings">
|
||||||
|
Via team
|
||||||
|
</span>
|
||||||
|
) : editingUser === p.user_id ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary"
|
||||||
@@ -286,7 +304,8 @@ export function AccessManagement({ projectName }: AccessManagementProps) {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -320,6 +320,8 @@ export interface UserUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Access Permission types
|
// Access Permission types
|
||||||
|
export type AccessSource = 'explicit' | 'team';
|
||||||
|
|
||||||
export interface AccessPermission {
|
export interface AccessPermission {
|
||||||
id: string;
|
id: string;
|
||||||
project_id: string;
|
project_id: string;
|
||||||
@@ -327,6 +329,9 @@ export interface AccessPermission {
|
|||||||
level: AccessLevel;
|
level: AccessLevel;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
expires_at: string | null;
|
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 {
|
export interface AccessPermissionCreate {
|
||||||
|
|||||||
Reference in New Issue
Block a user