nexo-schemas 0.0.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nexo/schemas/__init__.py +0 -0
- nexo/schemas/application.py +292 -0
- nexo/schemas/connection.py +134 -0
- nexo/schemas/data.py +27 -0
- nexo/schemas/document.py +237 -0
- nexo/schemas/error/__init__.py +476 -0
- nexo/schemas/error/constants.py +50 -0
- nexo/schemas/error/descriptor.py +354 -0
- nexo/schemas/error/enums.py +40 -0
- nexo/schemas/error/metadata.py +15 -0
- nexo/schemas/error/spec.py +312 -0
- nexo/schemas/exception/__init__.py +0 -0
- nexo/schemas/exception/exc.py +911 -0
- nexo/schemas/exception/factory.py +1928 -0
- nexo/schemas/exception/handlers.py +110 -0
- nexo/schemas/google.py +14 -0
- nexo/schemas/key/__init__.py +0 -0
- nexo/schemas/key/rsa.py +131 -0
- nexo/schemas/metadata.py +21 -0
- nexo/schemas/mixins/__init__.py +0 -0
- nexo/schemas/mixins/filter.py +140 -0
- nexo/schemas/mixins/general.py +65 -0
- nexo/schemas/mixins/hierarchy.py +19 -0
- nexo/schemas/mixins/identity.py +387 -0
- nexo/schemas/mixins/parameter.py +50 -0
- nexo/schemas/mixins/service.py +40 -0
- nexo/schemas/mixins/sort.py +111 -0
- nexo/schemas/mixins/timestamp.py +192 -0
- nexo/schemas/model.py +240 -0
- nexo/schemas/operation/__init__.py +0 -0
- nexo/schemas/operation/action/__init__.py +9 -0
- nexo/schemas/operation/action/base.py +14 -0
- nexo/schemas/operation/action/resource.py +371 -0
- nexo/schemas/operation/action/status.py +8 -0
- nexo/schemas/operation/action/system.py +6 -0
- nexo/schemas/operation/action/websocket.py +6 -0
- nexo/schemas/operation/base.py +289 -0
- nexo/schemas/operation/constants.py +18 -0
- nexo/schemas/operation/context.py +68 -0
- nexo/schemas/operation/dependency.py +26 -0
- nexo/schemas/operation/enums.py +168 -0
- nexo/schemas/operation/extractor.py +36 -0
- nexo/schemas/operation/mixins.py +53 -0
- nexo/schemas/operation/request.py +1066 -0
- nexo/schemas/operation/resource.py +839 -0
- nexo/schemas/operation/system.py +55 -0
- nexo/schemas/operation/websocket.py +55 -0
- nexo/schemas/pagination.py +67 -0
- nexo/schemas/parameter.py +60 -0
- nexo/schemas/payload.py +116 -0
- nexo/schemas/resource.py +64 -0
- nexo/schemas/response.py +1041 -0
- nexo/schemas/security/__init__.py +0 -0
- nexo/schemas/security/api_key.py +63 -0
- nexo/schemas/security/authentication.py +848 -0
- nexo/schemas/security/authorization.py +922 -0
- nexo/schemas/security/enums.py +32 -0
- nexo/schemas/security/impersonation.py +179 -0
- nexo/schemas/security/token.py +402 -0
- nexo/schemas/security/types.py +17 -0
- nexo/schemas/success/__init__.py +0 -0
- nexo/schemas/success/descriptor.py +100 -0
- nexo/schemas/success/enums.py +23 -0
- nexo/schemas/user_agent.py +46 -0
- nexo_schemas-0.0.16.dist-info/METADATA +87 -0
- nexo_schemas-0.0.16.dist-info/RECORD +69 -0
- nexo_schemas-0.0.16.dist-info/WHEEL +5 -0
- nexo_schemas-0.0.16.dist-info/licenses/LICENSE +21 -0
- nexo_schemas-0.0.16.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
from nexo.types.string import ListOfStrs
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Domain(StrEnum):
|
|
8
|
+
TENANT = "tenant"
|
|
9
|
+
SYSTEM = "system"
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def choices(cls) -> ListOfStrs:
|
|
13
|
+
return [e.value for e in cls]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DomainT = TypeVar("DomainT", bound=Domain)
|
|
17
|
+
OptDomain = Domain | None
|
|
18
|
+
OptDomainT = TypeVar("OptDomainT", bound=OptDomain)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DomainMixin(BaseModel, Generic[OptDomainT]):
|
|
22
|
+
domain: OptDomainT = Field(..., description="Domain")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RolePrefix(StrEnum):
|
|
26
|
+
MEDICAL = "medical"
|
|
27
|
+
TENANT = "tenant"
|
|
28
|
+
SYSTEM = "system"
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def choices(cls) -> ListOfStrs:
|
|
32
|
+
return [e.value for e in cls]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from fastapi import Header, Query
|
|
3
|
+
from fastapi.requests import HTTPConnection, Request
|
|
4
|
+
from fastapi.websockets import WebSocket
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from typing import (
|
|
7
|
+
Annotated,
|
|
8
|
+
Callable,
|
|
9
|
+
Generic,
|
|
10
|
+
Literal,
|
|
11
|
+
TypeVar,
|
|
12
|
+
overload,
|
|
13
|
+
)
|
|
14
|
+
from uuid import UUID
|
|
15
|
+
from nexo.enums.connection import Header as HeaderEnum, Protocol, OptProtocol
|
|
16
|
+
from nexo.types.string import ListOfStrs
|
|
17
|
+
from nexo.types.uuid import OptUUID
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Source(StrEnum):
|
|
21
|
+
HEADER = "header"
|
|
22
|
+
STATE = "state"
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def choices(cls) -> ListOfStrs:
|
|
26
|
+
return [e.value for e in cls]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Impersonation(BaseModel):
|
|
30
|
+
user_id: Annotated[UUID, Field(..., description="User's ID")]
|
|
31
|
+
organization_id: Annotated[
|
|
32
|
+
OptUUID, Field(None, description="Organization's ID")
|
|
33
|
+
] = None
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_header(cls, conn: HTTPConnection) -> "Impersonation | None":
|
|
37
|
+
organization_id = conn.headers.get(HeaderEnum.X_ORGANIZATION_ID, None)
|
|
38
|
+
if organization_id is not None:
|
|
39
|
+
organization_id = UUID(organization_id)
|
|
40
|
+
|
|
41
|
+
user_id = conn.headers.get(HeaderEnum.X_USER_ID, None)
|
|
42
|
+
if user_id is not None:
|
|
43
|
+
user_id = UUID(user_id)
|
|
44
|
+
|
|
45
|
+
return cls(
|
|
46
|
+
user_id=user_id,
|
|
47
|
+
organization_id=organization_id,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_query(cls, conn: HTTPConnection) -> "Impersonation | None":
|
|
54
|
+
organization_id = conn.query_params.get(
|
|
55
|
+
HeaderEnum.X_ORGANIZATION_ID.value, None
|
|
56
|
+
)
|
|
57
|
+
if organization_id is not None:
|
|
58
|
+
organization_id = UUID(organization_id)
|
|
59
|
+
|
|
60
|
+
user_id = conn.query_params.get(HeaderEnum.X_USER_ID.value, None)
|
|
61
|
+
if user_id is not None:
|
|
62
|
+
user_id = UUID(user_id)
|
|
63
|
+
|
|
64
|
+
return cls(
|
|
65
|
+
user_id=user_id,
|
|
66
|
+
organization_id=organization_id,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def extract(cls, conn: HTTPConnection) -> "Impersonation | None":
|
|
73
|
+
impersonation = getattr(conn.state, "impersonation", None)
|
|
74
|
+
if isinstance(impersonation, Impersonation):
|
|
75
|
+
return impersonation
|
|
76
|
+
|
|
77
|
+
impersonation = cls.from_header(conn)
|
|
78
|
+
if impersonation is not None:
|
|
79
|
+
return impersonation
|
|
80
|
+
|
|
81
|
+
impersonation = cls.from_query(conn)
|
|
82
|
+
if impersonation is not None:
|
|
83
|
+
return impersonation
|
|
84
|
+
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
@overload
|
|
88
|
+
@classmethod
|
|
89
|
+
def as_dependency(
|
|
90
|
+
cls,
|
|
91
|
+
protocol: None = None,
|
|
92
|
+
/,
|
|
93
|
+
) -> Callable[[HTTPConnection, OptUUID, OptUUID], "Impersonation | None"]: ...
|
|
94
|
+
@overload
|
|
95
|
+
@classmethod
|
|
96
|
+
def as_dependency(
|
|
97
|
+
cls,
|
|
98
|
+
protocol: Literal[Protocol.HTTP],
|
|
99
|
+
/,
|
|
100
|
+
) -> Callable[[Request, OptUUID, OptUUID], "Impersonation | None"]: ...
|
|
101
|
+
@overload
|
|
102
|
+
@classmethod
|
|
103
|
+
def as_dependency(
|
|
104
|
+
cls,
|
|
105
|
+
protocol: Literal[Protocol.WEBSOCKET],
|
|
106
|
+
/,
|
|
107
|
+
) -> Callable[[WebSocket, OptUUID, OptUUID], "Impersonation | None"]: ...
|
|
108
|
+
@classmethod
|
|
109
|
+
def as_dependency(
|
|
110
|
+
cls,
|
|
111
|
+
protocol: OptProtocol = None,
|
|
112
|
+
/,
|
|
113
|
+
) -> (
|
|
114
|
+
Callable[[HTTPConnection, OptUUID, OptUUID], "Impersonation | None"]
|
|
115
|
+
| Callable[[Request, OptUUID, OptUUID], "Impersonation | None"]
|
|
116
|
+
| Callable[[WebSocket, OptUUID, OptUUID], "Impersonation | None"]
|
|
117
|
+
):
|
|
118
|
+
def _dependency(
|
|
119
|
+
conn: HTTPConnection,
|
|
120
|
+
# These are for documentation purpose only
|
|
121
|
+
_user_id: OptUUID = Header(
|
|
122
|
+
None,
|
|
123
|
+
alias=HeaderEnum.X_USER_ID.value,
|
|
124
|
+
description="User's ID",
|
|
125
|
+
),
|
|
126
|
+
_organization_id: OptUUID = Header(
|
|
127
|
+
None,
|
|
128
|
+
alias=HeaderEnum.X_ORGANIZATION_ID.value,
|
|
129
|
+
description="Organization's ID",
|
|
130
|
+
),
|
|
131
|
+
) -> "Impersonation | None":
|
|
132
|
+
return cls.extract(conn)
|
|
133
|
+
|
|
134
|
+
def _request_dependency(
|
|
135
|
+
request: Request,
|
|
136
|
+
# These are for documentation purpose only
|
|
137
|
+
_user_id: OptUUID = Header(
|
|
138
|
+
None,
|
|
139
|
+
alias=HeaderEnum.X_USER_ID.value,
|
|
140
|
+
description="User's ID",
|
|
141
|
+
),
|
|
142
|
+
_organization_id: OptUUID = Header(
|
|
143
|
+
None,
|
|
144
|
+
alias=HeaderEnum.X_ORGANIZATION_ID.value,
|
|
145
|
+
description="Organization's ID",
|
|
146
|
+
),
|
|
147
|
+
) -> "Impersonation | None":
|
|
148
|
+
return cls.extract(request)
|
|
149
|
+
|
|
150
|
+
def _websocket_dependency(
|
|
151
|
+
websocket: WebSocket,
|
|
152
|
+
# These are for documentation purpose only
|
|
153
|
+
_user_id: OptUUID = Query(
|
|
154
|
+
None,
|
|
155
|
+
alias=HeaderEnum.X_USER_ID.value,
|
|
156
|
+
description="User's ID",
|
|
157
|
+
),
|
|
158
|
+
_organization_id: OptUUID = Query(
|
|
159
|
+
None,
|
|
160
|
+
alias=HeaderEnum.X_ORGANIZATION_ID.value,
|
|
161
|
+
description="Organization's ID",
|
|
162
|
+
),
|
|
163
|
+
) -> "Impersonation | None":
|
|
164
|
+
return cls.extract(websocket)
|
|
165
|
+
|
|
166
|
+
if protocol is None:
|
|
167
|
+
return _dependency
|
|
168
|
+
elif protocol is Protocol.HTTP:
|
|
169
|
+
return _request_dependency
|
|
170
|
+
elif protocol is Protocol.WEBSOCKET:
|
|
171
|
+
return _websocket_dependency
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
OptImpersonation = Impersonation | None
|
|
175
|
+
OptImpersonationT = TypeVar("OptImpersonationT", bound=OptImpersonation)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ImpersonationMixin(BaseModel, Generic[OptImpersonationT]):
|
|
179
|
+
impersonation: OptImpersonationT = Field(..., description="Impersonation")
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
from collections.abc import Iterable, Sequence
|
|
2
|
+
from Crypto.PublicKey.RSA import RsaKey
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from pydantic import BaseModel, Field, ValidationError, model_validator
|
|
5
|
+
from typing import (
|
|
6
|
+
Annotated,
|
|
7
|
+
Generic,
|
|
8
|
+
Literal,
|
|
9
|
+
Self,
|
|
10
|
+
TypeGuard,
|
|
11
|
+
TypeVar,
|
|
12
|
+
overload,
|
|
13
|
+
)
|
|
14
|
+
from uuid import UUID
|
|
15
|
+
from nexo.crypto.token import decode, encode
|
|
16
|
+
from nexo.enums.expiration import Expiration
|
|
17
|
+
from nexo.types.datetime import OptDatetime
|
|
18
|
+
from nexo.types.integer import OptInt, DoubleInts
|
|
19
|
+
from nexo.types.misc import BytesOrStr
|
|
20
|
+
from nexo.types.string import OptListOfStrs, OptStr
|
|
21
|
+
from nexo.types.uuid import OptUUID
|
|
22
|
+
from .enums import Domain, OptDomain, DomainT
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TokenV1(BaseModel):
|
|
26
|
+
iss: Annotated[OptStr, Field(None, description="Issuer")] = None
|
|
27
|
+
sub: Annotated[str, Field(..., description="Subject")]
|
|
28
|
+
sr: Annotated[str, Field(..., description="System role")]
|
|
29
|
+
u_i: Annotated[int, Field(..., description="User's ID")]
|
|
30
|
+
u_uu: Annotated[UUID, Field(..., description="User's UUID")]
|
|
31
|
+
u_u: Annotated[str, Field(..., description="User's Username")]
|
|
32
|
+
u_e: Annotated[str, Field(..., description="User's Email")]
|
|
33
|
+
u_ut: Annotated[str, Field(..., description="User's type")]
|
|
34
|
+
o_i: Annotated[OptInt, Field(None, description="Organization's ID")] = None
|
|
35
|
+
o_uu: Annotated[OptUUID, Field(None, description="Organization's UUID")] = None
|
|
36
|
+
o_k: Annotated[OptStr, Field(None, description="Organization's Key")] = None
|
|
37
|
+
o_ot: Annotated[OptStr, Field(None, description="Organization's type")] = None
|
|
38
|
+
uor: Annotated[
|
|
39
|
+
OptListOfStrs,
|
|
40
|
+
Field(None, description="User's organization role", min_length=1),
|
|
41
|
+
] = None
|
|
42
|
+
iat_dt: Annotated[datetime, Field(..., description="Issued At Timestamp")]
|
|
43
|
+
iat: Annotated[int, Field(..., description="Issued at")]
|
|
44
|
+
exp_dt: Annotated[datetime, Field(..., description="Expired At Timestamp")]
|
|
45
|
+
exp: Annotated[int, Field(..., description="Expired at")]
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_string(
|
|
49
|
+
cls,
|
|
50
|
+
token: str,
|
|
51
|
+
*,
|
|
52
|
+
key: BytesOrStr | RsaKey,
|
|
53
|
+
audience: str | Iterable[str] | None = None,
|
|
54
|
+
subject: OptStr = None,
|
|
55
|
+
issuer: str | Sequence[str] | None = None,
|
|
56
|
+
leeway: float | timedelta = 0,
|
|
57
|
+
) -> "TokenV1":
|
|
58
|
+
obj = decode(
|
|
59
|
+
token,
|
|
60
|
+
key=key,
|
|
61
|
+
audience=audience,
|
|
62
|
+
subject=subject,
|
|
63
|
+
issuer=issuer,
|
|
64
|
+
leeway=leeway,
|
|
65
|
+
)
|
|
66
|
+
return cls.model_validate(obj)
|
|
67
|
+
|
|
68
|
+
@overload
|
|
69
|
+
def to_string(
|
|
70
|
+
self,
|
|
71
|
+
key: RsaKey,
|
|
72
|
+
) -> str: ...
|
|
73
|
+
@overload
|
|
74
|
+
def to_string(
|
|
75
|
+
self,
|
|
76
|
+
key: BytesOrStr,
|
|
77
|
+
*,
|
|
78
|
+
password: OptStr = None,
|
|
79
|
+
) -> str: ...
|
|
80
|
+
@overload
|
|
81
|
+
def to_string(
|
|
82
|
+
self,
|
|
83
|
+
key: BytesOrStr | RsaKey,
|
|
84
|
+
*,
|
|
85
|
+
password: OptStr = None,
|
|
86
|
+
) -> str: ...
|
|
87
|
+
def to_string(
|
|
88
|
+
self,
|
|
89
|
+
key: BytesOrStr | RsaKey,
|
|
90
|
+
*,
|
|
91
|
+
password: OptStr = None,
|
|
92
|
+
) -> str:
|
|
93
|
+
if isinstance(key, RsaKey):
|
|
94
|
+
return encode(
|
|
95
|
+
payload=self.model_dump(mode="json", exclude_none=True),
|
|
96
|
+
key=key,
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
return encode(
|
|
100
|
+
payload=self.model_dump(mode="json", exclude_none=True),
|
|
101
|
+
key=key,
|
|
102
|
+
password=password,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Claim(BaseModel):
|
|
107
|
+
iss: Annotated[OptStr, Field(None, description="Issuer")] = None
|
|
108
|
+
sub: Annotated[UUID, Field(..., description="Subject")]
|
|
109
|
+
aud: Annotated[OptStr, Field(None, description="Audience")] = None
|
|
110
|
+
exp: Annotated[int, Field(..., description="Expired at")]
|
|
111
|
+
iat: Annotated[int, Field(..., description="Issued at")]
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def new_timestamp(
|
|
115
|
+
cls, iat_dt: OptDatetime = None, exp_in: Expiration = Expiration.EXP_15MN
|
|
116
|
+
) -> DoubleInts:
|
|
117
|
+
if iat_dt is None:
|
|
118
|
+
iat_dt = datetime.now(tz=timezone.utc)
|
|
119
|
+
exp_dt = iat_dt + timedelta(seconds=exp_in.value)
|
|
120
|
+
return int(iat_dt.timestamp()), int(exp_dt.timestamp())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
OrganizationT = TypeVar("OrganizationT", bound=OptUUID)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Credential(BaseModel, Generic[DomainT, OrganizationT]):
|
|
127
|
+
d: DomainT = Field(..., description="Domain")
|
|
128
|
+
o: OrganizationT = Field(..., description="Organization")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class GenericToken(
|
|
132
|
+
Credential[DomainT, OrganizationT],
|
|
133
|
+
Claim,
|
|
134
|
+
Generic[DomainT, OrganizationT],
|
|
135
|
+
):
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_string(
|
|
138
|
+
cls,
|
|
139
|
+
token: str,
|
|
140
|
+
*,
|
|
141
|
+
key: BytesOrStr | RsaKey,
|
|
142
|
+
audience: str | Iterable[str] | None = None,
|
|
143
|
+
subject: OptStr = None,
|
|
144
|
+
issuer: str | Sequence[str] | None = None,
|
|
145
|
+
leeway: float | timedelta = 0,
|
|
146
|
+
) -> Self:
|
|
147
|
+
obj = decode(
|
|
148
|
+
token,
|
|
149
|
+
key=key,
|
|
150
|
+
audience=audience,
|
|
151
|
+
subject=subject,
|
|
152
|
+
issuer=issuer,
|
|
153
|
+
leeway=leeway,
|
|
154
|
+
)
|
|
155
|
+
return cls.model_validate(obj)
|
|
156
|
+
|
|
157
|
+
@model_validator(mode="after")
|
|
158
|
+
def validate_credential(self) -> Self:
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
@overload
|
|
162
|
+
def to_string(
|
|
163
|
+
self,
|
|
164
|
+
key: RsaKey,
|
|
165
|
+
) -> str: ...
|
|
166
|
+
@overload
|
|
167
|
+
def to_string(
|
|
168
|
+
self,
|
|
169
|
+
key: BytesOrStr,
|
|
170
|
+
*,
|
|
171
|
+
password: OptStr = None,
|
|
172
|
+
) -> str: ...
|
|
173
|
+
@overload
|
|
174
|
+
def to_string(
|
|
175
|
+
self,
|
|
176
|
+
key: BytesOrStr | RsaKey,
|
|
177
|
+
*,
|
|
178
|
+
password: OptStr = None,
|
|
179
|
+
) -> str: ...
|
|
180
|
+
def to_string(
|
|
181
|
+
self,
|
|
182
|
+
key: BytesOrStr | RsaKey,
|
|
183
|
+
*,
|
|
184
|
+
password: OptStr = None,
|
|
185
|
+
) -> str:
|
|
186
|
+
if isinstance(key, RsaKey):
|
|
187
|
+
return encode(
|
|
188
|
+
payload=self.model_dump(mode="json", exclude_none=True),
|
|
189
|
+
key=key,
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
return encode(
|
|
193
|
+
payload=self.model_dump(mode="json", exclude_none=True),
|
|
194
|
+
key=key,
|
|
195
|
+
password=password,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TenantToken(GenericToken[Literal[Domain.TENANT], UUID]):
|
|
200
|
+
d: Annotated[Literal[Domain.TENANT], Field(Domain.TENANT, description="Domain")] = (
|
|
201
|
+
Domain.TENANT
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
@model_validator(mode="after")
|
|
205
|
+
def validate_identity(self) -> Self:
|
|
206
|
+
if self.d is not Domain.TENANT:
|
|
207
|
+
raise ValueError(f"Value of 'd' claim must be {Domain.TENANT}")
|
|
208
|
+
if not isinstance(self.o, UUID):
|
|
209
|
+
raise ValueError(f"Value of 'o' claim must be an UUID. Value: {self.o}")
|
|
210
|
+
return self
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def new(
|
|
214
|
+
cls,
|
|
215
|
+
*,
|
|
216
|
+
sub: UUID,
|
|
217
|
+
o: UUID,
|
|
218
|
+
iss: OptStr = None,
|
|
219
|
+
aud: OptStr = None,
|
|
220
|
+
iat_dt: OptDatetime = None,
|
|
221
|
+
exp_in: Expiration = Expiration.EXP_15MN,
|
|
222
|
+
) -> "TenantToken":
|
|
223
|
+
iat, exp = cls.new_timestamp(iat_dt, exp_in)
|
|
224
|
+
return cls(iss=iss, sub=sub, aud=aud, exp=exp, iat=iat, o=o)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class SystemToken(GenericToken[Literal[Domain.SYSTEM], None]):
|
|
228
|
+
d: Annotated[Literal[Domain.SYSTEM], Field(Domain.SYSTEM, description="Domain")] = (
|
|
229
|
+
Domain.SYSTEM
|
|
230
|
+
)
|
|
231
|
+
o: None = None
|
|
232
|
+
|
|
233
|
+
@model_validator(mode="after")
|
|
234
|
+
def validate_identity(self) -> Self:
|
|
235
|
+
if self.d is not Domain.SYSTEM:
|
|
236
|
+
raise ValueError(f"Value of 'd' claim must be {Domain.SYSTEM}")
|
|
237
|
+
if self.o is not None:
|
|
238
|
+
raise ValueError(f"Value of 'o' claim must be None. Value: {self.o}")
|
|
239
|
+
return self
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
def new(
|
|
243
|
+
cls,
|
|
244
|
+
*,
|
|
245
|
+
sub: UUID,
|
|
246
|
+
iss: OptStr = None,
|
|
247
|
+
aud: OptStr = None,
|
|
248
|
+
iat_dt: OptDatetime = None,
|
|
249
|
+
exp_in: Expiration = Expiration.EXP_15MN,
|
|
250
|
+
) -> "SystemToken":
|
|
251
|
+
iat, exp = cls.new_timestamp(iat_dt, exp_in)
|
|
252
|
+
return cls(iss=iss, sub=sub, aud=aud, exp=exp, iat=iat)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
AnyToken = TenantToken | SystemToken
|
|
256
|
+
AnyTokenT = TypeVar("AnyTokenT", bound=AnyToken)
|
|
257
|
+
OptAnyToken = AnyToken | None
|
|
258
|
+
OptAnyTokenT = TypeVar("OptAnyTokenT", bound=OptAnyToken)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class TokenMixin(BaseModel, Generic[OptAnyTokenT]):
|
|
262
|
+
token: OptAnyTokenT = Field(..., description="Token")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def is_tenant_token(token: AnyToken) -> TypeGuard[TenantToken]:
|
|
266
|
+
return (
|
|
267
|
+
isinstance(token, TenantToken)
|
|
268
|
+
and token.d is Domain.TENANT
|
|
269
|
+
and token.o is not None
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def is_system_token(token: AnyToken) -> TypeGuard[SystemToken]:
|
|
274
|
+
return (
|
|
275
|
+
isinstance(token, SystemToken) and token.d is Domain.SYSTEM and token.o is None
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class TokenFactory:
|
|
280
|
+
@overload
|
|
281
|
+
@staticmethod
|
|
282
|
+
def from_string(
|
|
283
|
+
token: str,
|
|
284
|
+
domain: Literal[Domain.TENANT],
|
|
285
|
+
*,
|
|
286
|
+
key: BytesOrStr | RsaKey,
|
|
287
|
+
audience: str | Iterable[str] | None = None,
|
|
288
|
+
subject: OptStr = None,
|
|
289
|
+
issuer: str | Sequence[str] | None = None,
|
|
290
|
+
leeway: float | timedelta = 0,
|
|
291
|
+
) -> TenantToken: ...
|
|
292
|
+
@overload
|
|
293
|
+
@staticmethod
|
|
294
|
+
def from_string(
|
|
295
|
+
token: str,
|
|
296
|
+
domain: Literal[Domain.SYSTEM],
|
|
297
|
+
*,
|
|
298
|
+
key: BytesOrStr | RsaKey,
|
|
299
|
+
audience: str | Iterable[str] | None = None,
|
|
300
|
+
subject: OptStr = None,
|
|
301
|
+
issuer: str | Sequence[str] | None = None,
|
|
302
|
+
leeway: float | timedelta = 0,
|
|
303
|
+
) -> SystemToken: ...
|
|
304
|
+
@overload
|
|
305
|
+
@staticmethod
|
|
306
|
+
def from_string(
|
|
307
|
+
token: str,
|
|
308
|
+
domain: None = None,
|
|
309
|
+
*,
|
|
310
|
+
key: BytesOrStr | RsaKey,
|
|
311
|
+
audience: str | Iterable[str] | None = None,
|
|
312
|
+
subject: OptStr = None,
|
|
313
|
+
issuer: str | Sequence[str] | None = None,
|
|
314
|
+
leeway: float | timedelta = 0,
|
|
315
|
+
) -> AnyToken: ...
|
|
316
|
+
@staticmethod
|
|
317
|
+
def from_string(
|
|
318
|
+
token: str,
|
|
319
|
+
domain: OptDomain = None,
|
|
320
|
+
*,
|
|
321
|
+
key: BytesOrStr | RsaKey,
|
|
322
|
+
audience: str | Iterable[str] | None = None,
|
|
323
|
+
subject: OptStr = None,
|
|
324
|
+
issuer: str | Sequence[str] | None = None,
|
|
325
|
+
leeway: float | timedelta = 0,
|
|
326
|
+
) -> AnyToken:
|
|
327
|
+
validated_token = None
|
|
328
|
+
models = (TokenV1, TenantToken, SystemToken)
|
|
329
|
+
for model in models:
|
|
330
|
+
try:
|
|
331
|
+
validated_token = model.from_string(
|
|
332
|
+
token,
|
|
333
|
+
key=key,
|
|
334
|
+
audience=audience,
|
|
335
|
+
subject=subject,
|
|
336
|
+
issuer=issuer,
|
|
337
|
+
leeway=leeway,
|
|
338
|
+
)
|
|
339
|
+
except ValidationError:
|
|
340
|
+
continue
|
|
341
|
+
if validated_token is None:
|
|
342
|
+
raise ValueError("Unable to validate raw token into known token model")
|
|
343
|
+
|
|
344
|
+
if isinstance(validated_token, (TenantToken, SystemToken)):
|
|
345
|
+
result_token = validated_token
|
|
346
|
+
else:
|
|
347
|
+
if validated_token.sr == "administrator":
|
|
348
|
+
if (
|
|
349
|
+
validated_token.o_i is not None
|
|
350
|
+
or validated_token.o_uu is not None
|
|
351
|
+
or validated_token.o_k is not None
|
|
352
|
+
or validated_token.o_ot is not None
|
|
353
|
+
or validated_token.uor is not None
|
|
354
|
+
):
|
|
355
|
+
raise ValueError(
|
|
356
|
+
"All organization-related claims must be None for System token"
|
|
357
|
+
)
|
|
358
|
+
result_token = SystemToken(
|
|
359
|
+
iss=validated_token.iss,
|
|
360
|
+
sub=validated_token.u_uu,
|
|
361
|
+
aud=None,
|
|
362
|
+
exp=validated_token.exp,
|
|
363
|
+
iat=validated_token.iat,
|
|
364
|
+
)
|
|
365
|
+
elif validated_token.sr == "user":
|
|
366
|
+
if (
|
|
367
|
+
validated_token.o_i is None
|
|
368
|
+
or validated_token.o_uu is None
|
|
369
|
+
or validated_token.o_k is None
|
|
370
|
+
or validated_token.o_ot is None
|
|
371
|
+
or validated_token.uor is None
|
|
372
|
+
):
|
|
373
|
+
raise ValueError(
|
|
374
|
+
"All organization-related claims can not be None for Tenant Token"
|
|
375
|
+
)
|
|
376
|
+
result_token = TenantToken(
|
|
377
|
+
iss=validated_token.iss,
|
|
378
|
+
sub=validated_token.u_uu,
|
|
379
|
+
aud=None,
|
|
380
|
+
exp=validated_token.exp,
|
|
381
|
+
iat=validated_token.iat,
|
|
382
|
+
o=validated_token.o_uu,
|
|
383
|
+
)
|
|
384
|
+
else:
|
|
385
|
+
raise ValueError(
|
|
386
|
+
f"Claim 'sr' can only be either 'administrator' or 'user' but received {validated_token.sr}"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if domain is None:
|
|
390
|
+
return result_token
|
|
391
|
+
elif domain is Domain.TENANT:
|
|
392
|
+
if not is_tenant_token(result_token):
|
|
393
|
+
raise ValueError(
|
|
394
|
+
"Failed parsing Tenant Token from string, raw token did not qualify as Tenant Token"
|
|
395
|
+
)
|
|
396
|
+
return result_token
|
|
397
|
+
elif domain is Domain.SYSTEM:
|
|
398
|
+
if not is_system_token(result_token):
|
|
399
|
+
raise ValueError(
|
|
400
|
+
"Failed parsing System token from string, raw token did not qualify as System Token"
|
|
401
|
+
)
|
|
402
|
+
return result_token
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
from nexo.enums.organization import ListOfOrganizationRoles, SeqOfOrganizationRoles
|
|
3
|
+
from nexo.enums.system import ListOfSystemRoles, SeqOfSystemRoles
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
ListOfDomainRoles = ListOfOrganizationRoles | ListOfSystemRoles
|
|
7
|
+
ListOfDomainRolesT = TypeVar("ListOfDomainRolesT", bound=ListOfDomainRoles)
|
|
8
|
+
|
|
9
|
+
OptListOfDomainRoles = ListOfDomainRoles | None
|
|
10
|
+
OptListOfDomainRolesT = TypeVar("OptListOfDomainRolesT", bound=OptListOfDomainRoles)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SeqOfDomainRoles = SeqOfOrganizationRoles | SeqOfSystemRoles
|
|
14
|
+
SeqOfDomainRolesT = TypeVar("SeqOfDomainRolesT", bound=SeqOfDomainRoles)
|
|
15
|
+
|
|
16
|
+
OptSeqOfDomainRoles = SeqOfDomainRoles | None
|
|
17
|
+
OptSeqOfDomainRolesT = TypeVar("OptSeqOfDomainRolesT", bound=OptSeqOfDomainRoles)
|
|
File without changes
|