Skip to content

Commit 4a48793

Browse files
Merge pull request #63 from minvws/feat/possible-scopes
feat: allowed scopes as part of application config
2 parents 044f519 + 98b8a3d commit 4a48793

7 files changed

Lines changed: 55 additions & 10 deletions

File tree

app.conf.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
[app]
22
# Loglevel can be one of: debug, info, warning, error, critical
33
loglevel=debug
4+
# list of possible scopes according to the NVI
5+
scopes = nvi:create nvi:read nvi:update nvi:delete nvi:localize
46

57
[database]
68
# Dsn for database connection

app/config.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import logging
33
import os
44
from enum import Enum
5-
from typing import Any
5+
from typing import Any, List
66

7-
from pydantic import BaseModel, Field, ValidationError
7+
from pydantic import BaseModel, Field, ValidationError, field_validator
88

99
logger = logging.getLogger(__name__)
1010

@@ -24,6 +24,16 @@ class LogLevel(str, Enum):
2424

2525
class ConfigApp(BaseModel):
2626
loglevel: LogLevel = Field(default=LogLevel.info)
27+
scopes: List[str] = Field(default=[])
28+
29+
@field_validator("scopes", mode="before")
30+
@classmethod
31+
def validate_scopes(cls, value: Any) -> List[str]:
32+
if not isinstance(value, str):
33+
raise ValueError("only space separated str are allowed. Check config file..")
34+
35+
trimmed_value = value.lstrip().rstrip()
36+
return trimmed_value.split(" ")
2737

2838

2939
class ConfigDatabase(BaseModel):

app/container.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def container_config(binder: inject.Binder) -> None:
1717
db = Database(config_database=config.database)
1818
binder.bind(Database, db)
1919

20-
organization_service = OrganizationService(db)
20+
organization_service = OrganizationService(db, config.app.scopes)
2121
binder.bind(OrganizationService, organization_service)
2222

2323
client_service = ClientService(db, organization_service)

app/scope_utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from typing import List
2+
3+
14
def parse(value: str | None) -> set[str]:
25
if not value:
36
return set()
@@ -6,3 +9,9 @@ def parse(value: str | None) -> set[str]:
69

710
def is_subset(child: str | None, parent: str | None) -> bool:
811
return parse(child).issubset(parse(parent))
12+
13+
14+
def check_allowed(allowed: List[str], requested: str) -> bool:
15+
sanitized = requested.lstrip().rstrip().split(" ")
16+
17+
return set(sanitized).issubset(set(allowed))

app/services/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
class ScopesNotGrantedError(Exception):
22
def __init__(self, ungranted: set[str]) -> None:
33
super().__init__(f"Scopes not granted by the organization: {', '.join(sorted(ungranted))}")
4+
5+
6+
class ScopeNotAllowedError(Exception):
7+
def __init__(self, scope: str) -> None:
8+
super().__init__(f"Scope {scope} is not allowed")

app/services/organization.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
from datetime import datetime
2-
from typing import List
2+
from typing import Any, List
33
from uuid import UUID
44

55
from app import scope_utils
66
from app.db.db import Database
77
from app.db.models.organization import OrganizationEntity
88
from app.db.repository.organization import OrganizationRepository
99
from app.models.ura import UraNumber
10-
from app.services.exceptions import ScopesNotGrantedError
10+
from app.services.exceptions import ScopeNotAllowedError, ScopesNotGrantedError
1111

1212

1313
class OrganizationService:
14-
def __init__(self, db: Database) -> None:
14+
def __init__(self, db: Database, allowed_scopes: List[str]) -> None:
1515
self.db = db
16+
self.allowed_scopes = allowed_scopes
1617

1718
def create_one(
1819
self,
1920
register_id: UraNumber,
2021
name: str,
2122
scopes: str | None = None,
2223
) -> OrganizationEntity:
24+
if scopes:
25+
scope_allowed = scope_utils.check_allowed(self.allowed_scopes, scopes)
26+
if not scope_allowed:
27+
raise ScopeNotAllowedError(scopes)
28+
2329
with self.db.get_db_session() as session:
2430
repo = session.get_repository(OrganizationRepository)
2531
entity = OrganizationEntity(register_id=register_id, name=name, scopes=scopes)
@@ -45,10 +51,21 @@ def get_many(
4551
with self.db.get_db_session() as session:
4652
repo = session.get_repository(OrganizationRepository)
4753
return list(
48-
repo.get_many(register_id=register_id, name=name, scopes=scopes, include_deleted=include_deleted)
54+
repo.get_many(
55+
register_id=register_id,
56+
name=name,
57+
scopes=scopes,
58+
include_deleted=include_deleted,
59+
)
4960
)
5061

51-
def update_one(self, id: UUID, **kwargs: object) -> OrganizationEntity | None:
62+
def update_one(self, id: UUID, **kwargs: Any) -> OrganizationEntity | None:
63+
if "scopes" in kwargs:
64+
scopes: str = kwargs["scopes"]
65+
valid_scope = scope_utils.check_allowed(self.allowed_scopes, scopes)
66+
if not valid_scope:
67+
raise ScopeNotAllowedError(scopes)
68+
5269
with self.db.get_db_session() as session:
5370
repo = session.get_repository(OrganizationRepository)
5471
return repo.update(id, **kwargs)

tests/conftest.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def client_repository(database: Database) -> ClientRepository:
5252

5353
@pytest.fixture()
5454
def organization_service(database: Database) -> OrganizationService:
55-
return OrganizationService(database)
55+
return OrganizationService(database, allowed_scopes=["read", "write", "delete"])
5656

5757

5858
@pytest.fixture()
@@ -66,7 +66,9 @@ def organization_entity() -> OrganizationEntity:
6666

6767

6868
@pytest.fixture()
69-
def persisted_organization(organization_service: OrganizationService) -> OrganizationEntity:
69+
def persisted_organization(
70+
organization_service: OrganizationService,
71+
) -> OrganizationEntity:
7072
return organization_service.create_one(register_id=TEST_REGISTER_ID, name=TEST_ORG_NAME)
7173

7274

0 commit comments

Comments
 (0)