Skip to content

Commit 5fb32c7

Browse files
UpstreamDatab-rowan
authored andcommitted
Change permissions to strings, and add better parsing method.
Use strings for permissions as they are more extensible than enums. Less granular permissions are also available, `*` is all permissions, `software` or `software.*` gives access to all software endpoints, and `software.read` gives access to read-only software information. `*.read` can also be used to give read access to all endpoints.
1 parent 118d176 commit 5fb32c7

File tree

15 files changed

+66
-138
lines changed

15 files changed

+66
-138
lines changed

goosebit/api/v1/devices/device/routes.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,22 @@
55

66
from goosebit.api.v1.devices.device.responses import DeviceLogResponse, DeviceResponse
77
from goosebit.auth import validate_user_permissions
8-
from goosebit.permissions import Permissions
98
from goosebit.updater.manager import UpdateManager, get_update_manager
109

1110
router = APIRouter(prefix="/{dev_id}")
1211

1312

1413
@router.get(
1514
"",
16-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.HOME.READ])],
15+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
1716
)
1817
async def device_get(_: Request, updater: UpdateManager = Depends(get_update_manager)) -> DeviceResponse:
1918
return await DeviceResponse.convert(await updater.get_device())
2019

2120

2221
@router.get(
2322
"/log",
24-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.HOME.READ])],
23+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
2524
)
2625
async def device_logs(_: Request, updater: UpdateManager = Depends(get_update_manager)) -> DeviceLogResponse:
2726
device = await updater.get_device()

goosebit/api/v1/devices/routes.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from goosebit.api.responses import StatusResponse
77
from goosebit.auth import validate_user_permissions
88
from goosebit.models import Device, Software, UpdateModeEnum
9-
from goosebit.permissions import Permissions
109
from goosebit.updater.manager import delete_devices, get_update_manager
1110

1211
from . import device
@@ -18,15 +17,15 @@
1817

1918
@router.get(
2019
"",
21-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.HOME.READ])],
20+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
2221
)
2322
async def devices_get(_: Request) -> DevicesResponse:
2423
return await DevicesResponse.convert(await Device.all().prefetch_related("assigned_software", "hardware"))
2524

2625

2726
@router.patch(
2827
"",
29-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.DEVICE.WRITE])],
28+
dependencies=[Security(validate_user_permissions, scopes=["device.write"])],
3029
)
3130
async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusResponse:
3231
for uuid in config.devices:
@@ -52,7 +51,7 @@ async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusRespon
5251

5352
@router.delete(
5453
"",
55-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.DEVICE.DELETE])],
54+
dependencies=[Security(validate_user_permissions, scopes=["device.delete"])],
5655
)
5756
async def devices_delete(_: Request, config: DevicesDeleteRequest) -> StatusResponse:
5857
await delete_devices(config.devices)

goosebit/api/v1/rollouts/routes.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from goosebit.api.responses import StatusResponse
55
from goosebit.auth import validate_user_permissions
66
from goosebit.models import Rollout
7-
from goosebit.permissions import Permissions
87

98
from .requests import RolloutsDeleteRequest, RolloutsPatchRequest, RolloutsPutRequest
109
from .responses import RolloutsPutResponse, RolloutsResponse
@@ -14,15 +13,15 @@
1413

1514
@router.get(
1615
"",
17-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.READ])],
16+
dependencies=[Security(validate_user_permissions, scopes=["rollout.read"])],
1817
)
1918
async def rollouts_get(_: Request) -> RolloutsResponse:
2019
return await RolloutsResponse.convert(await Rollout.all().prefetch_related("software"))
2120

2221

2322
@router.post(
2423
"",
25-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.WRITE])],
24+
dependencies=[Security(validate_user_permissions, scopes=["rollout.write"])],
2625
)
2726
async def rollouts_put(_: Request, rollout: RolloutsPutRequest) -> RolloutsPutResponse:
2827
rollout = await Rollout.create(
@@ -35,7 +34,7 @@ async def rollouts_put(_: Request, rollout: RolloutsPutRequest) -> RolloutsPutRe
3534

3635
@router.patch(
3736
"",
38-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.WRITE])],
37+
dependencies=[Security(validate_user_permissions, scopes=["rollout.write"])],
3938
)
4039
async def rollouts_patch(_: Request, rollouts: RolloutsPatchRequest) -> StatusResponse:
4140
await Rollout.filter(id__in=rollouts.ids).update(paused=rollouts.paused)
@@ -44,7 +43,7 @@ async def rollouts_patch(_: Request, rollouts: RolloutsPatchRequest) -> StatusRe
4443

4544
@router.delete(
4645
"",
47-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.DELETE])],
46+
dependencies=[Security(validate_user_permissions, scopes=["rollout.delete"])],
4847
)
4948
async def rollouts_delete(_: Request, rollouts: RolloutsDeleteRequest) -> StatusResponse:
5049
await Rollout.filter(id__in=rollouts.ids).delete()

goosebit/api/v1/software/routes.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from goosebit.api.responses import StatusResponse
55
from goosebit.auth import validate_user_permissions
66
from goosebit.models import Rollout, Software
7-
from goosebit.permissions import Permissions
87

98
from .requests import SoftwareDeleteRequest
109
from .responses import SoftwareResponse
@@ -14,15 +13,15 @@
1413

1514
@router.get(
1615
"",
17-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.SOFTWARE.READ])],
16+
dependencies=[Security(validate_user_permissions, scopes=["software.read"])],
1817
)
1918
async def software_get(_: Request) -> SoftwareResponse:
2019
return await SoftwareResponse.convert(await Software.all().prefetch_related("compatibility"))
2120

2221

2322
@router.delete(
2423
"",
25-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.SOFTWARE.DELETE])],
24+
dependencies=[Security(validate_user_permissions, scopes=["software.delete"])],
2625
)
2726
async def software_delete(_: Request, config: SoftwareDeleteRequest) -> StatusResponse:
2827
success = False

goosebit/auth/__init__.py

+27-10
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,30 @@ def validate_user_permissions(
106106
security: SecurityScopes,
107107
user: User = Depends(get_current_user),
108108
) -> HTTPConnection:
109-
if security.scopes is None:
110-
return connection
111-
for scope in security.scopes:
112-
if scope not in user.permissions:
113-
logger.warning(f"User {user.username} does not have permission {scope}")
114-
raise HTTPException(
115-
status_code=403,
116-
detail="Not enough permissions",
117-
headers={"WWW-Authenticate": "Bearer"},
118-
)
109+
if not compare_permissions(security.scopes, user.permissions):
110+
logger.warning(f"{user.username} does not have sufficient permissions")
111+
raise HTTPException(
112+
status_code=403,
113+
detail="Not enough permissions",
114+
headers={"WWW-Authenticate": "Bearer"},
115+
)
116+
return connection
117+
118+
119+
def compare_permissions(scopes: list[str] | None, permissions: set[str]) -> bool:
120+
if scopes is None:
121+
return True
122+
for scope in scopes:
123+
if not any([compare_permission(scope, permission) for permission in permissions]):
124+
return False
125+
return True
126+
127+
128+
def compare_permission(scope: str, permission: str):
129+
split_scope = scope.split(".")
130+
for idx, permission in enumerate(permission.split(".")):
131+
if permission == "*":
132+
continue
133+
if not split_scope[idx] == permission:
134+
return False
135+
return True

goosebit/permissions.py

-77
This file was deleted.

goosebit/realtime/logs.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from websockets.exceptions import ConnectionClosed
77

88
from goosebit.auth import validate_user_permissions
9-
from goosebit.permissions import Permissions
109
from goosebit.updater.manager import get_update_manager
1110

1211
router = APIRouter(prefix="/logs")
@@ -20,7 +19,7 @@ class RealtimeLogModel(BaseModel):
2019

2120
@router.websocket(
2221
"/{dev_id}",
23-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.HOME.READ])],
22+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
2423
)
2524
async def device_logs(websocket: WebSocket, dev_id: str):
2625
await websocket.accept()

goosebit/settings/schema.py

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import secrets
22
from pathlib import Path
3-
from typing import Annotated, Iterable
3+
from typing import Annotated
44

55
from joserfc.rfc7518.oct_key import OctKey
66
from pydantic import BaseModel, BeforeValidator, Field
@@ -11,22 +11,13 @@
1111
YamlConfigSettingsSource,
1212
)
1313

14-
from goosebit.permissions import Permissions, PermissionsBase
15-
1614
from .const import BASE_DIR, LOGGING_DEFAULT, PWD_CXT
1715

1816

19-
def parse_permissions(items: Iterable[str]):
20-
permissions = set()
21-
for p in items:
22-
permissions.update(Permissions.from_str(p))
23-
return permissions
24-
25-
2617
class User(BaseModel):
2718
username: str
2819
hashed_pwd: Annotated[str, BeforeValidator(PWD_CXT.hash)] = Field(validation_alias="password")
29-
permissions: Annotated[set[PermissionsBase], BeforeValidator(parse_permissions)]
20+
permissions: set[str]
3021

3122
def get_json_permissions(self):
3223
return [str(p) for p in self.permissions]

goosebit/ui/bff/devices/routes.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from goosebit.auth import validate_user_permissions
88
from goosebit.models import Device, UpdateModeEnum, UpdateStateEnum
9-
from goosebit.permissions import Permissions
109

1110
from .responses import BFFDeviceResponse
1211

@@ -15,7 +14,7 @@
1514

1615
@router.get(
1716
"",
18-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.HOME.READ])],
17+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
1918
)
2019
async def devices_get(request: Request) -> BFFDeviceResponse:
2120
def search_filter(search_value):

goosebit/ui/bff/rollouts/routes.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from goosebit.auth import validate_user_permissions
66
from goosebit.models import Rollout
7-
from goosebit.permissions import Permissions
87

98
from .responses import BFFRolloutsResponse
109

@@ -13,7 +12,7 @@
1312

1413
@router.get(
1514
"",
16-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.READ])],
15+
dependencies=[Security(validate_user_permissions, scopes=["rollout.read"])],
1716
)
1817
async def rollouts_get(request: Request) -> BFFRolloutsResponse:
1918
def search_filter(search_value):

goosebit/ui/bff/software/routes.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66

77
from goosebit.auth import validate_user_permissions
88
from goosebit.models import Software
9-
from goosebit.permissions import Permissions
109
from goosebit.ui.bff.software.responses import BFFSoftwareResponse
1110

1211
router = APIRouter(prefix="/software")
1312

1413

1514
@router.get(
1615
"",
17-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.SOFTWARE.READ])],
16+
dependencies=[Security(validate_user_permissions, scopes=["software.read"])],
1817
)
1918
async def software_get(request: Request) -> BFFSoftwareResponse:
2019
def search_filter(search_value):

0 commit comments

Comments
 (0)