Compare commits
5 commits
662b9c8e26
...
d6c14655c0
| Author | SHA1 | Date | |
|---|---|---|---|
| d6c14655c0 | |||
| 4b384db98a | |||
| 327f857190 | |||
| 154870acb1 | |||
| f96cb2112c |
8 changed files with 172 additions and 17 deletions
|
|
@ -42,7 +42,9 @@ class Permission(Base):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
service_rel = relationship("Service", foreign_keys="Permission.service_id")
|
service_rel = relationship(
|
||||||
|
"Service", back_populates="permission_rel", foreign_keys="Permission.service_id"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def service_name(self):
|
def service_name(self):
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ from sqlalchemy.exc import IntegrityError
|
||||||
from psycopg.errors import UniqueViolation
|
from psycopg.errors import UniqueViolation
|
||||||
|
|
||||||
from src.iam.exceptions import GroupNotFoundException
|
from src.iam.exceptions import GroupNotFoundException
|
||||||
|
from src.organisation.dependencies import org_model_body_dependency
|
||||||
from src.organisation.exceptions import OrgNotFoundException
|
from src.organisation.exceptions import OrgNotFoundException
|
||||||
from src.schemas import GroupSummary, OrgSummary, ResourceName
|
from src.schemas import GroupSummary, OrgSummary, ResourceName
|
||||||
from src.service.dependencies import service_model_body_dependency
|
from src.service.dependencies import service_model_body_dependency
|
||||||
|
|
@ -82,6 +83,8 @@ from src.iam.schemas import (
|
||||||
IAMCAoRResponse,
|
IAMCAoRResponse,
|
||||||
IAMPutGroupInvitationAcceptResponse,
|
IAMPutGroupInvitationAcceptResponse,
|
||||||
IAMPutGroupInvitationResponse,
|
IAMPutGroupInvitationResponse,
|
||||||
|
IAMPutOrgPermissionsRequest,
|
||||||
|
IAMPutOrgPermissionsResponse,
|
||||||
)
|
)
|
||||||
from src.utils import verify_email_token
|
from src.utils import verify_email_token
|
||||||
|
|
||||||
|
|
@ -547,7 +550,7 @@ async def delete_permission(
|
||||||
response_model=IAMGetPermissionsSearchResponse,
|
response_model=IAMGetPermissionsSearchResponse,
|
||||||
responses={},
|
responses={},
|
||||||
)
|
)
|
||||||
async def post_permissions(
|
async def permissions_search(
|
||||||
db: db_dependency,
|
db: db_dependency,
|
||||||
org_model: org_model_root_claim_body_dependency,
|
org_model: org_model_root_claim_body_dependency,
|
||||||
request_model: IAMGetPermissionsSearchRequest,
|
request_model: IAMGetPermissionsSearchRequest,
|
||||||
|
|
@ -672,3 +675,36 @@ async def accept_invitation(
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
path="/org/permissions",
|
||||||
|
summary="Grants an org access to permissions",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
response_model=IAMPutOrgPermissionsResponse,
|
||||||
|
responses={
|
||||||
|
status.HTTP_401_UNAUTHORIZED: {"description": "Must be super user."},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def add_org_permissions(
|
||||||
|
db: db_dependency,
|
||||||
|
su: super_admin_dependency,
|
||||||
|
org_model: org_model_body_dependency,
|
||||||
|
request_model: IAMPutOrgPermissionsRequest,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Grants a permission to a group. Returns a list of the permissions in the group as well as a summary for the org and group.
|
||||||
|
"""
|
||||||
|
for permission in request_model.permissions:
|
||||||
|
perm_model = db.get(Perm, permission)
|
||||||
|
|
||||||
|
if perm_model not in org_model.permission_rel:
|
||||||
|
org_model.permission_rel.append(perm_model)
|
||||||
|
|
||||||
|
db.flush()
|
||||||
|
response = IAMPutOrgPermissionsResponse(
|
||||||
|
organisation=OrgSummary(**org_model.__dict__),
|
||||||
|
permissions=org_model.permission_rel,
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return response
|
||||||
|
|
|
||||||
|
|
@ -150,3 +150,12 @@ class IAMPutGroupInvitationAcceptResponse(CustomBaseModel):
|
||||||
organisation: OrgSummary
|
organisation: OrgSummary
|
||||||
user: UserSummary
|
user: UserSummary
|
||||||
group: GroupDetails
|
group: GroupDetails
|
||||||
|
|
||||||
|
|
||||||
|
class IAMPutOrgPermissionsRequest(OrgIDMixin):
|
||||||
|
permissions: list[int]
|
||||||
|
|
||||||
|
|
||||||
|
class IAMPutOrgPermissionsResponse(CustomBaseModel):
|
||||||
|
organisation: OrgSummary
|
||||||
|
permissions: list[PermissionSchema]
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,19 @@ Exports:
|
||||||
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from fastapi import Request, Depends
|
||||||
|
|
||||||
from src.service.models import Service
|
|
||||||
from src.database import db_dependency
|
from src.database import db_dependency
|
||||||
from src.exceptions import UnauthorizedException
|
from src.exceptions import UnauthorizedException
|
||||||
from src.utils import send_email, generate_jwt
|
from src.utils import send_email, generate_jwt
|
||||||
|
|
||||||
from src.iam.schemas import IAMCAoRRequest
|
|
||||||
from src.iam.models import Group
|
from src.iam.models import Group
|
||||||
|
|
||||||
|
from src.service.models import Service
|
||||||
from fastapi import Request, Depends
|
from src.service.schemas import HasServiceName
|
||||||
|
|
||||||
|
|
||||||
def valid_service_key(
|
def valid_service_key(
|
||||||
db: db_dependency, request: Request, request_model: IAMCAoRRequest
|
db: db_dependency, request: Request, request_model: HasServiceName
|
||||||
) -> bool:
|
) -> bool:
|
||||||
rn = request_model.rn
|
rn = request_model.rn
|
||||||
api_key = request.headers.get("X-API-Key", None)
|
api_key = request.headers.get("X-API-Key", None)
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,6 @@ class CustomBaseModel(BaseModel):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ResourceName(CustomBaseModel):
|
|
||||||
service: str
|
|
||||||
organisation: str
|
|
||||||
resource: str
|
|
||||||
instance: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
### Mixins ###
|
### Mixins ###
|
||||||
class OrgIDMixin(CustomBaseModel):
|
class OrgIDMixin(CustomBaseModel):
|
||||||
organisation_id: int = Field(gt=0)
|
organisation_id: int = Field(gt=0)
|
||||||
|
|
@ -42,6 +35,10 @@ class UserIDMixin(CustomBaseModel):
|
||||||
user_id: int = Field(gt=0)
|
user_id: int = Field(gt=0)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceNameMixin(CustomBaseModel):
|
||||||
|
service: str
|
||||||
|
|
||||||
|
|
||||||
class OrgSummary(CustomBaseModel):
|
class OrgSummary(CustomBaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
|
|
@ -60,3 +57,9 @@ class UserSummary(CustomBaseModel):
|
||||||
class ServiceSummary(CustomBaseModel):
|
class ServiceSummary(CustomBaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceName(ServiceNameMixin):
|
||||||
|
organisation: str
|
||||||
|
resource: str
|
||||||
|
instance: Optional[str] = None
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ Models:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, String
|
from sqlalchemy import Column, Integer, String
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from src.database import Base
|
from src.database import Base
|
||||||
|
|
||||||
|
|
@ -17,3 +18,5 @@ class Service(Base):
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
name = Column(String, unique=True)
|
name = Column(String, unique=True)
|
||||||
api_key = Column(String, unique=True)
|
api_key = Column(String, unique=True)
|
||||||
|
|
||||||
|
permission_rel = relationship("Permission", back_populates="service_rel")
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ from src.auth.dependencies import (
|
||||||
super_admin_dependency,
|
super_admin_dependency,
|
||||||
org_model_root_claim_query_dependency,
|
org_model_root_claim_query_dependency,
|
||||||
)
|
)
|
||||||
|
from src.iam.service import service_key_dependency
|
||||||
|
from src.iam.models import Permission as Perm
|
||||||
|
from src.service.exceptions import ServiceNotFoundException
|
||||||
|
|
||||||
from src.service.models import Service
|
from src.service.models import Service
|
||||||
from src.service.utils import generate_api_key
|
from src.service.utils import generate_api_key
|
||||||
|
|
@ -32,6 +35,8 @@ from src.service.schemas import (
|
||||||
ServiceWithKeySchema,
|
ServiceWithKeySchema,
|
||||||
ServicePatchKeyResponse,
|
ServicePatchKeyResponse,
|
||||||
ServicePatchKeyRequest,
|
ServicePatchKeyRequest,
|
||||||
|
ServicePostPermissionsResponse,
|
||||||
|
ServicePostPermissionsRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
|
|
@ -170,3 +175,66 @@ async def remove_service(
|
||||||
"""
|
"""
|
||||||
db.delete(service_model)
|
db.delete(service_model)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
path="/permissions",
|
||||||
|
summary="Service endpoint for creating its own permissions.",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
response_model=ServicePostPermissionsResponse,
|
||||||
|
responses={
|
||||||
|
status.HTTP_401_UNAUTHORIZED: {
|
||||||
|
"description": "API Key missing or invalid | Issue verifying user OIDC claims"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def service_create_new_permissions(
|
||||||
|
db: db_dependency,
|
||||||
|
request_model: ServicePostPermissionsRequest,
|
||||||
|
valid_key: service_key_dependency,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Allows a service to register its own set of permissions.
|
||||||
|
"""
|
||||||
|
service_model = (
|
||||||
|
db.query(Service).filter(Service.name == request_model.rn.service).first()
|
||||||
|
)
|
||||||
|
if service_model is None:
|
||||||
|
raise ServiceNotFoundException()
|
||||||
|
else:
|
||||||
|
service_id = service_model.id
|
||||||
|
response_list = []
|
||||||
|
for new_permission in request_model.permissions:
|
||||||
|
perm_model = (
|
||||||
|
db.query(Perm)
|
||||||
|
.filter(Perm.service_id == service_id)
|
||||||
|
.filter(Perm.resource == new_permission.resource)
|
||||||
|
.filter(Perm.action == new_permission.action)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if perm_model is not None:
|
||||||
|
response_code = 409
|
||||||
|
response = {
|
||||||
|
"id": perm_model.id,
|
||||||
|
"service_name": perm_model.service_name,
|
||||||
|
"resource": perm_model.resource,
|
||||||
|
"action": perm_model.action,
|
||||||
|
}
|
||||||
|
response_list.append((response, response_code))
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_perm_model = Perm(**new_permission.__dict__)
|
||||||
|
new_perm_model.service_id = service_id
|
||||||
|
db.add(new_perm_model)
|
||||||
|
db.flush()
|
||||||
|
response_code = 201
|
||||||
|
response = {
|
||||||
|
"id": new_perm_model.id,
|
||||||
|
"service_name": new_perm_model.service_name,
|
||||||
|
"resource": new_perm_model.resource,
|
||||||
|
"action": new_perm_model.action,
|
||||||
|
}
|
||||||
|
response_list.append((response, response_code))
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return {"permissions": response_list}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,36 @@ Models follow the nomenclature of:
|
||||||
- Models: "<Module><Method><Resource><Opt:Resource><Direction>" ie "ServiceGetServiceResponse"
|
- Models: "<Module><Method><Resource><Opt:Resource><Direction>" ie "ServiceGetServiceResponse"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import Field
|
from typing import Generic, TypeVar
|
||||||
|
from pydantic import Field, ConfigDict
|
||||||
|
|
||||||
from src.schemas import CustomBaseModel, ServiceIDMixin, ServiceSummary
|
from src.schemas import (
|
||||||
|
CustomBaseModel,
|
||||||
|
ServiceIDMixin,
|
||||||
|
ServiceSummary,
|
||||||
|
ServiceNameMixin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=ServiceNameMixin)
|
||||||
|
|
||||||
|
|
||||||
|
class HasServiceName(CustomBaseModel, Generic[T]):
|
||||||
|
rn: T
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionResponseSchema(CustomBaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
||||||
|
|
||||||
|
id: int
|
||||||
|
service_name: str
|
||||||
|
resource: str
|
||||||
|
action: str
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionRequestSchema(CustomBaseModel):
|
||||||
|
resource: str
|
||||||
|
action: str
|
||||||
|
|
||||||
|
|
||||||
class ServiceWithKeySchema(ServiceSummary):
|
class ServiceWithKeySchema(ServiceSummary):
|
||||||
|
|
@ -33,3 +60,12 @@ class ServicePatchKeyRequest(ServiceIDMixin):
|
||||||
|
|
||||||
class ServicePatchKeyResponse(CustomBaseModel):
|
class ServicePatchKeyResponse(CustomBaseModel):
|
||||||
service: ServiceWithKeySchema
|
service: ServiceWithKeySchema
|
||||||
|
|
||||||
|
|
||||||
|
class ServicePostPermissionsRequest(CustomBaseModel):
|
||||||
|
rn: ServiceNameMixin
|
||||||
|
permissions: list[PermissionRequestSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class ServicePostPermissionsResponse(CustomBaseModel):
|
||||||
|
permissions: list[tuple[PermissionResponseSchema, int]]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue