Add access permission management API
Backend:
- Add AccessPermission schemas (Create, Update, Response)
- Add ProjectWithAccessResponse schema
- Add permission endpoints:
- GET /project/{name}/permissions - list permissions (admin only)
- POST /project/{name}/permissions - grant access (admin only)
- PUT /project/{name}/permissions/{username} - update access
- DELETE /project/{name}/permissions/{username} - revoke access
- GET /project/{name}/my-access - get current user's access level
Frontend:
- Add AccessLevel, AccessPermission types
- Add API functions for access management:
- getMyProjectAccess()
- listProjectPermissions()
- grantProjectAccess()
- updateProjectAccess()
- revokeProjectAccess()
This commit is contained in:
@@ -107,6 +107,9 @@ from .schemas import (
|
|||||||
APIKeyCreate,
|
APIKeyCreate,
|
||||||
APIKeyResponse,
|
APIKeyResponse,
|
||||||
APIKeyCreateResponse,
|
APIKeyCreateResponse,
|
||||||
|
AccessPermissionCreate,
|
||||||
|
AccessPermissionUpdate,
|
||||||
|
AccessPermissionResponse,
|
||||||
)
|
)
|
||||||
from .metadata import extract_metadata
|
from .metadata import extract_metadata
|
||||||
from .config import get_settings
|
from .config import get_settings
|
||||||
@@ -1190,6 +1193,159 @@ def delete_project(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Access Permission routes
|
||||||
|
@router.get(
|
||||||
|
"/api/v1/project/{project_name}/permissions",
|
||||||
|
response_model=List[AccessPermissionResponse],
|
||||||
|
)
|
||||||
|
def list_project_permissions(
|
||||||
|
project_name: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
List all access permissions for a project.
|
||||||
|
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))
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/api/v1/project/{project_name}/permissions",
|
||||||
|
response_model=AccessPermissionResponse,
|
||||||
|
)
|
||||||
|
def grant_project_access(
|
||||||
|
project_name: str,
|
||||||
|
permission: AccessPermissionCreate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Grant access to a user for a project.
|
||||||
|
Requires admin access to the project.
|
||||||
|
"""
|
||||||
|
project = check_project_access(db, project_name, current_user, "admin")
|
||||||
|
|
||||||
|
auth_service = AuthorizationService(db)
|
||||||
|
new_permission = auth_service.grant_access(
|
||||||
|
str(project.id),
|
||||||
|
permission.username,
|
||||||
|
permission.level,
|
||||||
|
permission.expires_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
return new_permission
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/api/v1/project/{project_name}/permissions/{username}",
|
||||||
|
response_model=AccessPermissionResponse,
|
||||||
|
)
|
||||||
|
def update_project_access(
|
||||||
|
project_name: str,
|
||||||
|
username: str,
|
||||||
|
permission: AccessPermissionUpdate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update a user's access level for a project.
|
||||||
|
Requires admin access to the project.
|
||||||
|
"""
|
||||||
|
project = check_project_access(db, project_name, current_user, "admin")
|
||||||
|
|
||||||
|
auth_service = AuthorizationService(db)
|
||||||
|
|
||||||
|
# Get existing permission
|
||||||
|
from .models import AccessPermission
|
||||||
|
existing = (
|
||||||
|
db.query(AccessPermission)
|
||||||
|
.filter(
|
||||||
|
AccessPermission.project_id == project.id,
|
||||||
|
AccessPermission.user_id == username,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not existing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"No access permission found for user '{username}'",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update fields
|
||||||
|
if permission.level is not None:
|
||||||
|
existing.level = permission.level
|
||||||
|
if permission.expires_at is not None:
|
||||||
|
existing.expires_at = permission.expires_at
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(existing)
|
||||||
|
|
||||||
|
return existing
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/api/v1/project/{project_name}/permissions/{username}",
|
||||||
|
status_code=204,
|
||||||
|
)
|
||||||
|
def revoke_project_access(
|
||||||
|
project_name: str,
|
||||||
|
username: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Revoke a user's access to a project.
|
||||||
|
Requires admin access to the project.
|
||||||
|
"""
|
||||||
|
project = check_project_access(db, project_name, current_user, "admin")
|
||||||
|
|
||||||
|
auth_service = AuthorizationService(db)
|
||||||
|
deleted = auth_service.revoke_access(str(project.id), username)
|
||||||
|
|
||||||
|
if not deleted:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"No access permission found for user '{username}'",
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/api/v1/project/{project_name}/my-access",
|
||||||
|
)
|
||||||
|
def get_my_project_access(
|
||||||
|
project_name: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: Optional[User] = Depends(get_current_user_optional),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get the current user's access level for a project.
|
||||||
|
Returns null for anonymous users on private projects.
|
||||||
|
"""
|
||||||
|
from .models import Project
|
||||||
|
|
||||||
|
project = db.query(Project).filter(Project.name == project_name).first()
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(status_code=404, detail="Project not found")
|
||||||
|
|
||||||
|
auth_service = AuthorizationService(db)
|
||||||
|
access_level = auth_service.get_user_access_level(str(project.id), current_user)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"project": project_name,
|
||||||
|
"access_level": access_level,
|
||||||
|
"is_owner": current_user and project.created_by == current_user.username,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Package routes
|
# Package routes
|
||||||
@router.get(
|
@router.get(
|
||||||
"/api/v1/project/{project_name}/packages",
|
"/api/v1/project/{project_name}/packages",
|
||||||
|
|||||||
@@ -776,3 +776,49 @@ class APIKeyCreateResponse(BaseModel):
|
|||||||
created_at: datetime
|
created_at: datetime
|
||||||
expires_at: Optional[datetime]
|
expires_at: Optional[datetime]
|
||||||
|
|
||||||
|
|
||||||
|
# Access Permission schemas
|
||||||
|
class AccessPermissionCreate(BaseModel):
|
||||||
|
"""Grant access to a user for a project"""
|
||||||
|
username: str
|
||||||
|
level: str # 'read', 'write', or 'admin'
|
||||||
|
expires_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
@field_validator('level')
|
||||||
|
@classmethod
|
||||||
|
def validate_level(cls, v):
|
||||||
|
if v not in ('read', 'write', 'admin'):
|
||||||
|
raise ValueError("level must be 'read', 'write', or 'admin'")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class AccessPermissionUpdate(BaseModel):
|
||||||
|
"""Update access permission"""
|
||||||
|
level: Optional[str] = None
|
||||||
|
expires_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
@field_validator('level')
|
||||||
|
@classmethod
|
||||||
|
def validate_level(cls, v):
|
||||||
|
if v is not None and v not in ('read', 'write', 'admin'):
|
||||||
|
raise ValueError("level must be 'read', 'write', or 'admin'")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class AccessPermissionResponse(BaseModel):
|
||||||
|
"""Access permission response"""
|
||||||
|
id: UUID
|
||||||
|
project_id: UUID
|
||||||
|
user_id: str
|
||||||
|
level: str
|
||||||
|
created_at: datetime
|
||||||
|
expires_at: Optional[datetime]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectWithAccessResponse(ProjectResponse):
|
||||||
|
"""Project response with user's access level"""
|
||||||
|
user_access_level: Optional[str] = None
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import {
|
|||||||
AdminUser,
|
AdminUser,
|
||||||
UserCreate,
|
UserCreate,
|
||||||
UserUpdate,
|
UserUpdate,
|
||||||
|
AccessPermission,
|
||||||
|
AccessPermissionCreate,
|
||||||
|
AccessPermissionUpdate,
|
||||||
|
AccessLevel,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const API_BASE = '/api/v1';
|
const API_BASE = '/api/v1';
|
||||||
@@ -299,3 +303,62 @@ export async function resetUserPassword(username: string, newPassword: string):
|
|||||||
throw new Error(error.detail || `HTTP ${response.status}`);
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Access Permission API
|
||||||
|
export interface MyAccessResponse {
|
||||||
|
project: string;
|
||||||
|
access_level: AccessLevel | null;
|
||||||
|
is_owner: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMyProjectAccess(projectName: string): Promise<MyAccessResponse> {
|
||||||
|
const response = await fetch(`${API_BASE}/project/${projectName}/my-access`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
return handleResponse<MyAccessResponse>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listProjectPermissions(projectName: string): Promise<AccessPermission[]> {
|
||||||
|
const response = await fetch(`${API_BASE}/project/${projectName}/permissions`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
return handleResponse<AccessPermission[]>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function grantProjectAccess(
|
||||||
|
projectName: string,
|
||||||
|
data: AccessPermissionCreate
|
||||||
|
): Promise<AccessPermission> {
|
||||||
|
const response = await fetch(`${API_BASE}/project/${projectName}/permissions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
return handleResponse<AccessPermission>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateProjectAccess(
|
||||||
|
projectName: string,
|
||||||
|
username: string,
|
||||||
|
data: AccessPermissionUpdate
|
||||||
|
): Promise<AccessPermission> {
|
||||||
|
const response = await fetch(`${API_BASE}/project/${projectName}/permissions/${username}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
return handleResponse<AccessPermission>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function revokeProjectAccess(projectName: string, username: string): Promise<void> {
|
||||||
|
const response = await fetch(`${API_BASE}/project/${projectName}/permissions/${username}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
||||||
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -289,3 +289,38 @@ export interface UserUpdate {
|
|||||||
is_admin?: boolean;
|
is_admin?: boolean;
|
||||||
is_active?: boolean;
|
is_active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Access Control types
|
||||||
|
export type AccessLevel = 'read' | 'write' | 'admin';
|
||||||
|
|
||||||
|
export interface AccessPermission {
|
||||||
|
id: string;
|
||||||
|
project_id: string;
|
||||||
|
user_id: string;
|
||||||
|
level: AccessLevel;
|
||||||
|
created_at: string;
|
||||||
|
expires_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessPermissionCreate {
|
||||||
|
username: string;
|
||||||
|
level: AccessLevel;
|
||||||
|
expires_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessPermissionUpdate {
|
||||||
|
level?: AccessLevel;
|
||||||
|
expires_at?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extended Project with user's access level
|
||||||
|
export interface ProjectWithAccess extends Project {
|
||||||
|
user_access_level?: AccessLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current user with permissions context
|
||||||
|
export interface CurrentUser extends User {
|
||||||
|
permissions?: {
|
||||||
|
[projectId: string]: AccessLevel;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user