apexauthlib 0.1.9__py3-none-any.whl → 0.1.11__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.
- apexauthlib/entities/auth.py +30 -0
- apexauthlib/integration/api.py +55 -1
- apexauthlib/integration/formatter.py +58 -0
- apexauthlib-0.1.11.dist-info/METADATA +132 -0
- apexauthlib-0.1.11.dist-info/RECORD +12 -0
- {apexauthlib-0.1.9.dist-info → apexauthlib-0.1.11.dist-info}/WHEEL +1 -1
- apexauthlib-0.1.9.dist-info/METADATA +0 -19
- apexauthlib-0.1.9.dist-info/RECORD +0 -11
- {apexauthlib-0.1.9.dist-info → apexauthlib-0.1.11.dist-info/licenses}/LICENSE +0 -0
apexauthlib/entities/auth.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
2
5
|
from typing import Any, Generic, TypeVar
|
|
3
6
|
from uuid import uuid4
|
|
4
7
|
|
|
@@ -48,3 +51,30 @@ class ServiceUserInfo(Generic[ItemT]):
|
|
|
48
51
|
user: User
|
|
49
52
|
is_service_admin: bool
|
|
50
53
|
metadata: ItemT
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class ServicePermission:
|
|
58
|
+
name: str
|
|
59
|
+
label: str
|
|
60
|
+
description: str
|
|
61
|
+
type: FieldType
|
|
62
|
+
default: str | int | bool
|
|
63
|
+
|
|
64
|
+
values: list[str] | list[int] = field(default_factory=list)
|
|
65
|
+
id: str = field(default_factory=lambda: str(uuid4()))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class FieldType(str, Enum):
|
|
69
|
+
string = "string"
|
|
70
|
+
integer = "integer"
|
|
71
|
+
boolean = "boolean"
|
|
72
|
+
|
|
73
|
+
def valid_type(self) -> type:
|
|
74
|
+
if self.value == FieldType.string:
|
|
75
|
+
return str
|
|
76
|
+
if self.value == FieldType.integer:
|
|
77
|
+
return int
|
|
78
|
+
if self.value == FieldType.boolean:
|
|
79
|
+
return bool
|
|
80
|
+
raise ValueError(f"Invalid type {self.value}")
|
apexauthlib/integration/api.py
CHANGED
|
@@ -7,7 +7,8 @@ from apexdevkit.formatter import DataclassFormatter, Formatter
|
|
|
7
7
|
from apexdevkit.http import FluentHttp, JsonDict
|
|
8
8
|
|
|
9
9
|
from apexauthlib.entities import User
|
|
10
|
-
from apexauthlib.entities.auth import ItemT, ServiceUserInfo
|
|
10
|
+
from apexauthlib.entities.auth import ItemT, ServicePermission, ServiceUserInfo
|
|
11
|
+
from apexauthlib.integration.formatter import ServicePermissionFormatter
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@dataclass(frozen=True)
|
|
@@ -118,6 +119,59 @@ class AuthApi(Generic[ItemT]):
|
|
|
118
119
|
metadata=self.formatter.load(raw_user["metadata"]),
|
|
119
120
|
)
|
|
120
121
|
|
|
122
|
+
def update_permissions(
|
|
123
|
+
self, permissions: list[ServicePermission]
|
|
124
|
+
) -> list[ServicePermission]:
|
|
125
|
+
existing = {
|
|
126
|
+
permission.name: permission
|
|
127
|
+
for permission in self._retrieve_existing_permissions()
|
|
128
|
+
}
|
|
129
|
+
new = {new_permission.name: new_permission for new_permission in permissions}
|
|
130
|
+
|
|
131
|
+
for current, current_permission in existing.items():
|
|
132
|
+
if current not in new.keys():
|
|
133
|
+
self._delete_permission(current_permission.id)
|
|
134
|
+
|
|
135
|
+
for current, current_permission in new.items():
|
|
136
|
+
if current not in existing.keys():
|
|
137
|
+
self._create_permission(current_permission)
|
|
138
|
+
|
|
139
|
+
return self._retrieve_existing_permissions()
|
|
140
|
+
|
|
141
|
+
def _retrieve_existing_permissions(self) -> list[ServicePermission]:
|
|
142
|
+
existing = list(
|
|
143
|
+
JsonDict(
|
|
144
|
+
(
|
|
145
|
+
self.http.with_header("Authorization", f"Bearer {self.token}")
|
|
146
|
+
.get()
|
|
147
|
+
.on_endpoint(f"/services/{self.service_name}/permissions")
|
|
148
|
+
.on_failure(raises=RuntimeError)
|
|
149
|
+
.json()
|
|
150
|
+
)
|
|
151
|
+
)["data"]["permissions"]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return [ServicePermissionFormatter().load(item) for item in existing]
|
|
155
|
+
|
|
156
|
+
def _delete_permission(self, permission_id: str) -> None:
|
|
157
|
+
(
|
|
158
|
+
self.http.with_header("Authorization", f"Bearer {self.token}")
|
|
159
|
+
.delete()
|
|
160
|
+
.on_endpoint(f"/services/{self.service_name}/permissions/{permission_id}")
|
|
161
|
+
.on_failure(raises=RuntimeError)
|
|
162
|
+
.json()
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _create_permission(self, permission: ServicePermission) -> None:
|
|
166
|
+
(
|
|
167
|
+
self.http.with_header("Authorization", f"Bearer {self.token}")
|
|
168
|
+
.with_json(JsonDict(ServicePermissionFormatter().dump(permission)))
|
|
169
|
+
.post()
|
|
170
|
+
.on_endpoint(f"/services/{self.service_name}/permissions")
|
|
171
|
+
.on_failure(raises=RuntimeError)
|
|
172
|
+
.json()
|
|
173
|
+
)
|
|
174
|
+
|
|
121
175
|
|
|
122
176
|
@dataclass
|
|
123
177
|
class AuthCodeApi:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Mapping
|
|
3
|
+
|
|
4
|
+
from apexauthlib.entities.auth import FieldType, ServicePermission
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class ServicePermissionFormatter:
|
|
9
|
+
def dump(self, permission: ServicePermission) -> Mapping[str, Any]:
|
|
10
|
+
raw = {
|
|
11
|
+
"name": permission.name,
|
|
12
|
+
"description": permission.description,
|
|
13
|
+
"label": permission.label,
|
|
14
|
+
"type": permission.type.value,
|
|
15
|
+
"values": permission.values,
|
|
16
|
+
"default": permission.default,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if permission.id.isdigit():
|
|
20
|
+
raw["id"] = permission.id
|
|
21
|
+
|
|
22
|
+
return raw
|
|
23
|
+
|
|
24
|
+
def load(self, raw: Mapping[str, Any]) -> ServicePermission:
|
|
25
|
+
permission_type = FieldType[str(raw["type"])]
|
|
26
|
+
|
|
27
|
+
return ServicePermission(
|
|
28
|
+
id=str(raw["id"]),
|
|
29
|
+
name=str(raw["name"]),
|
|
30
|
+
label=str(raw["label"]),
|
|
31
|
+
description=str(raw["description"]),
|
|
32
|
+
type=permission_type,
|
|
33
|
+
values=self._load_combos(permission_type, raw),
|
|
34
|
+
default=self._load_default(permission_type, raw),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def _load_combos(
|
|
38
|
+
self, permission_type: FieldType, raw: Mapping[str, Any]
|
|
39
|
+
) -> list[str] | list[int]:
|
|
40
|
+
if permission_type == FieldType.boolean:
|
|
41
|
+
return []
|
|
42
|
+
if permission_type == FieldType.string:
|
|
43
|
+
return [str(item) for item in list(raw["values"])]
|
|
44
|
+
if permission_type == FieldType.integer:
|
|
45
|
+
return [int(item) for item in list(raw["values"])]
|
|
46
|
+
|
|
47
|
+
raise ValueError(f"Unexpected type: {permission_type}")
|
|
48
|
+
|
|
49
|
+
def _load_default(
|
|
50
|
+
self, permission_type: FieldType, raw: Mapping[str, Any]
|
|
51
|
+
) -> str | int | bool:
|
|
52
|
+
if permission_type == FieldType.string:
|
|
53
|
+
return str(raw["default"])
|
|
54
|
+
if permission_type == FieldType.integer:
|
|
55
|
+
return int(raw["default"])
|
|
56
|
+
if permission_type == FieldType.boolean:
|
|
57
|
+
return bool(raw["default"])
|
|
58
|
+
raise ValueError(f"Unexpected type: {permission_type}")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apexauthlib
|
|
3
|
+
Version: 0.1.11
|
|
4
|
+
Summary: Apex authorization library for services
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Author: Apex Dev
|
|
7
|
+
Author-email: dev@apex.ge
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: apexdevkit
|
|
14
|
+
Requires-Dist: fastapi (==0.120.*)
|
|
15
|
+
Requires-Dist: httpx
|
|
16
|
+
Requires-Dist: uvicorn
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# apexauthlib
|
|
20
|
+
|
|
21
|
+
Private Python library for integrating backend services with the company auth-service.
|
|
22
|
+
|
|
23
|
+
This library provides:
|
|
24
|
+
- **Entities** used across services (user, service, client, service metadata, permissions).
|
|
25
|
+
- A **FastAPI router** (`auth_api`) with login endpoints and dependency helpers that inject the current user + metadata from JWT.
|
|
26
|
+
- A small **HTTP client wrapper** (`AuthApiProvider` / `AuthApi`) for services to:
|
|
27
|
+
- obtain a token (password login or OAuth code exchange),
|
|
28
|
+
- fetch current user and service-scoped metadata,
|
|
29
|
+
- list users for a service,
|
|
30
|
+
- and register/update service permissions.
|
|
31
|
+
|
|
32
|
+
> Notes
|
|
33
|
+
> - This documentation is best used along with the documentation for auth-service.
|
|
34
|
+
> - This is a **private library**. If you change it, you should push to GitHub and then update downstream usage by running `make install` wherever it’s consumed (per team workflow).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Concepts at a glance
|
|
39
|
+
|
|
40
|
+
### Users
|
|
41
|
+
The `User` entity includes:
|
|
42
|
+
- `hashed_password`: **must never expose an actual password** through APIs (except in create/update requests handled elsewhere).
|
|
43
|
+
- `is_admin`: indicates whether the user is a “superadmin”.
|
|
44
|
+
|
|
45
|
+
### Service-scoped auth model
|
|
46
|
+
The auth model used by services is service-centric:
|
|
47
|
+
- Services have a name (`service_name`) and service admins.
|
|
48
|
+
- Each service can define **permissions** (schema) and **per-user metadata** values that should align with those permissions.
|
|
49
|
+
|
|
50
|
+
See: [`docs/concepts.md`](docs/concepts.md)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Usage pattern (high-level)
|
|
55
|
+
|
|
56
|
+
### 1) Create an `AuthApiProvider`
|
|
57
|
+
|
|
58
|
+
Example (from service code):
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
provider = AuthApiProvider[HaccpPermissions](
|
|
62
|
+
http=FluentHttp(
|
|
63
|
+
Httpx.Builder().with_url(AUTH_SERVICE_API).build(),
|
|
64
|
+
),
|
|
65
|
+
service_name=SERVICE_NAME,
|
|
66
|
+
formatter=DataclassFormatter(HaccpPermissions),
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2) Register permissions (service schema)
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
provider.for_token(
|
|
76
|
+
provider.login(ADMIN_USERNAME, ADMIN_PASSWORD)
|
|
77
|
+
).update_permissions(
|
|
78
|
+
[
|
|
79
|
+
ServicePermission(
|
|
80
|
+
# ...
|
|
81
|
+
)
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3) Add FastAPI auth routes + dependencies
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
FastApiBuilder()
|
|
92
|
+
.with_title(TITLE)
|
|
93
|
+
.with_description(DESCRIPTION)
|
|
94
|
+
.with_version(VERSION)
|
|
95
|
+
.with_route(auth_api)
|
|
96
|
+
.with_dependency(
|
|
97
|
+
auth=provider,
|
|
98
|
+
auth_code=AuthCodeApi(
|
|
99
|
+
http=FluentHttp(
|
|
100
|
+
Httpx.Builder()
|
|
101
|
+
.with_url(AUTH_SERVICE_API)
|
|
102
|
+
.build(),
|
|
103
|
+
),
|
|
104
|
+
client_id=CLIENT_ID,
|
|
105
|
+
client_secret=CLIENT_SECRET,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
.build()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
See: [`docs/fastapi.md`](docs/fastapi.md)
|
|
112
|
+
|
|
113
|
+
## Known Limitations
|
|
114
|
+
|
|
115
|
+
### Permission sync is name-based only
|
|
116
|
+
|
|
117
|
+
AuthApi.update_permissions() compares permissions by name and only creates/deletes.
|
|
118
|
+
If a permission keeps the same name but changes type, default, label, etc.,
|
|
119
|
+
it currently won’t be updated automatically.
|
|
120
|
+
|
|
121
|
+
### Admin password dependency for permission registration
|
|
122
|
+
|
|
123
|
+
The example uses provider.login(ADMIN_USERNAME, ADMIN_PASSWORD) to obtain a token for updating permissions.
|
|
124
|
+
If the admin password changes, downstream services must update their configuration, or a more proper flow should
|
|
125
|
+
be introduced (e.g., service-to-service auth or code-based admin auth).
|
|
126
|
+
|
|
127
|
+
### Refresh tokens are not currently handled in this library
|
|
128
|
+
|
|
129
|
+
Current flow assumes a bearer access token is provided/used. Supporting refresh tokens would enable
|
|
130
|
+
longer-lived sessions with automatic token renewal.
|
|
131
|
+
|
|
132
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
apexauthlib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
apexauthlib/entities/__init__.py,sha256=KghMo6a2QWdPzWlMqdGAKOaVvRmTCOIVhXtIHrX7A_M,149
|
|
3
|
+
apexauthlib/entities/auth.py,sha256=AY34GnOhVhu0WCrpp0LoJ__r6ou-hORFiVExvCyCzmE,1566
|
|
4
|
+
apexauthlib/fastapi/__init__.py,sha256=G77C3ZIqjsXdh4fiH5FZyF_hyzVtS3waJJGa_TBGMWE,103
|
|
5
|
+
apexauthlib/fastapi/auth.py,sha256=hkvG2BougydElkYbEpwsuSgRoLOLANe6DbVSoa-zBRM,3917
|
|
6
|
+
apexauthlib/integration/__init__.py,sha256=f2lGbyoGct4kpZ2CUTExHhtQHs-1YR_xanvrj9Y4GiI,87
|
|
7
|
+
apexauthlib/integration/api.py,sha256=iSjaq6FrLOvnC86Y4DtV8O2fNqy09Tt5HjyAp707C4I,6478
|
|
8
|
+
apexauthlib/integration/formatter.py,sha256=DyVRMWYkZErZBeP_f4aYdSbe9g9zck-x2ONFPxwVNfY,2037
|
|
9
|
+
apexauthlib-0.1.11.dist-info/METADATA,sha256=2iH1MMTn76woy7R_XuungNbggbxFuhkuBv8bBf00mik,3922
|
|
10
|
+
apexauthlib-0.1.11.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
11
|
+
apexauthlib-0.1.11.dist-info/licenses/LICENSE,sha256=iai0ILQTDgUXV1cIXl0UzSeOdFpMFK3shn5aqnz_Uro,1065
|
|
12
|
+
apexauthlib-0.1.11.dist-info/RECORD,,
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: apexauthlib
|
|
3
|
-
Version: 0.1.9
|
|
4
|
-
Summary: Apex authorization library for services
|
|
5
|
-
Author: Apex Dev
|
|
6
|
-
Author-email: dev@apex.ge
|
|
7
|
-
Requires-Python: >=3.11
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
-
Requires-Dist: apexdevkit
|
|
12
|
-
Requires-Dist: fastapi
|
|
13
|
-
Requires-Dist: httpx
|
|
14
|
-
Requires-Dist: uvicorn
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
|
|
17
|
-
# apexauthlib
|
|
18
|
-
Central authorization library for apex services
|
|
19
|
-
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
apexauthlib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
apexauthlib/entities/__init__.py,sha256=KghMo6a2QWdPzWlMqdGAKOaVvRmTCOIVhXtIHrX7A_M,149
|
|
3
|
-
apexauthlib/entities/auth.py,sha256=Zyme1bgQEW1SHLeHSjTaY8E4yd12EIZOEOE3cICZOAo,843
|
|
4
|
-
apexauthlib/fastapi/__init__.py,sha256=G77C3ZIqjsXdh4fiH5FZyF_hyzVtS3waJJGa_TBGMWE,103
|
|
5
|
-
apexauthlib/fastapi/auth.py,sha256=hkvG2BougydElkYbEpwsuSgRoLOLANe6DbVSoa-zBRM,3917
|
|
6
|
-
apexauthlib/integration/__init__.py,sha256=f2lGbyoGct4kpZ2CUTExHhtQHs-1YR_xanvrj9Y4GiI,87
|
|
7
|
-
apexauthlib/integration/api.py,sha256=c94T9Z7IPM0K1zKDjex6fr9TNOtE3ZLHnjT2x8kOodY,4372
|
|
8
|
-
apexauthlib-0.1.9.dist-info/LICENSE,sha256=iai0ILQTDgUXV1cIXl0UzSeOdFpMFK3shn5aqnz_Uro,1065
|
|
9
|
-
apexauthlib-0.1.9.dist-info/METADATA,sha256=jTSqo4KFSZGv2Ew58Dm52xQq6myqcAp7vbfTS667C64,518
|
|
10
|
-
apexauthlib-0.1.9.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
11
|
-
apexauthlib-0.1.9.dist-info/RECORD,,
|
|
File without changes
|