auth 1.2.0__tar.gz → 1.3.0__tar.gz
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.
- {auth-1.2.0 → auth-1.3.0}/PKG-INFO +3 -1
- {auth-1.2.0 → auth-1.3.0}/auth/services/service.py +113 -123
- {auth-1.2.0 → auth-1.3.0}/auth.egg-info/PKG-INFO +3 -1
- {auth-1.2.0 → auth-1.3.0}/auth.egg-info/requires.txt +2 -0
- {auth-1.2.0 → auth-1.3.0}/pyproject.toml +3 -1
- {auth-1.2.0 → auth-1.3.0}/LICENSE +0 -0
- {auth-1.2.0 → auth-1.3.0}/README.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/__init__.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/audit.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/circuit_breaker.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/client.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/cmd/__init__.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/cmd/server.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/config.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/core/REST/__init__.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/core/REST/client.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/core/__init__.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/core/models/__init__.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/dal/authorization_sqlite.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/database.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/decorators.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/encryption.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/jwt_auth.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/logging_config.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/main.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/models/sql.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/models/sqlite.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/response_format.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/routes.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/sanitizer.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/server.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/services/rest_service.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/validation.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth/workflow_checker.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth.egg-info/SOURCES.txt +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth.egg-info/dependency_links.txt +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth.egg-info/entry_points.txt +0 -0
- {auth-1.2.0 → auth-1.3.0}/auth.egg-info/top_level.txt +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/api/authorization.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/api/client.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/api/database.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/api/models.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/api/service.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/audit_logging.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/changelog.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/concepts.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/conf.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/configuration.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/contributing.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/deployment.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/encryption.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/examples.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/index.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/installation.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/production.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/python_usage.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/quickstart.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/rest_api.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/security.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/docs/troubleshooting.rst +0 -0
- {auth-1.2.0 → auth-1.3.0}/scripts/create_db.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/setup.cfg +0 -0
- {auth-1.2.0 → auth-1.3.0}/tests/test_auth_sqlite.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/tests/test_authorization_sqlite.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/tests/test_client_rest.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/tests/test_cmd_server.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/tests/test_db_sqlite.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/tests/test_flask.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/tests/test_server.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/tests/test_service_rest.py +0 -0
- {auth-1.2.0 → auth-1.3.0}/tests/tests.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: auth
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Authorization for humans
|
|
5
5
|
Author-email: Farshid Ashouri <farsheed.ashouri@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -33,6 +33,8 @@ Requires-Dist: PyJWT>=2.0.0
|
|
|
33
33
|
Requires-Dist: cryptography>=3.0.0
|
|
34
34
|
Requires-Dist: APScheduler>=3.0.0
|
|
35
35
|
Requires-Dist: psycopg[binary]>=3.0.0
|
|
36
|
+
Requires-Dist: pydantic>=2.0.0
|
|
37
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
36
38
|
Provides-Extra: dev
|
|
37
39
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
38
40
|
Requires-Dist: pytest-cov>=2.0; extra == "dev"
|
|
@@ -165,45 +165,32 @@ class AuthorizationService:
|
|
|
165
165
|
return result
|
|
166
166
|
|
|
167
167
|
def add_role(self, role: str, description: Optional[str] = None) -> bool:
|
|
168
|
-
"""Add a new role
|
|
169
|
-
# Check if role already exists
|
|
170
|
-
existing = (
|
|
171
|
-
self.db.query(AuthGroup)
|
|
172
|
-
.filter(AuthGroup.creator == self.client, AuthGroup.role == role)
|
|
173
|
-
.first()
|
|
174
|
-
)
|
|
168
|
+
"""Add a new role - atomic idempotent using ON CONFLICT.
|
|
175
169
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
existing.description = description
|
|
181
|
-
self.db.commit()
|
|
182
|
-
return True
|
|
183
|
-
# For compatibility with old tests, return True if role already exists
|
|
184
|
-
return True
|
|
170
|
+
Uses PostgreSQL's INSERT ... ON CONFLICT for race-condition-free upsert.
|
|
171
|
+
The unique constraint (creator, role) ensures atomicity.
|
|
172
|
+
"""
|
|
173
|
+
from sqlalchemy import text
|
|
185
174
|
|
|
186
175
|
try:
|
|
187
|
-
|
|
188
|
-
|
|
176
|
+
self.db.execute(
|
|
177
|
+
text("""
|
|
178
|
+
INSERT INTO auth_rbac.auth_group
|
|
179
|
+
(creator, role, description, is_active, date_created, modified)
|
|
180
|
+
VALUES
|
|
181
|
+
(:creator, :role, :description, true, NOW(), NOW())
|
|
182
|
+
ON CONFLICT (creator, role) DO UPDATE SET
|
|
183
|
+
is_active = true,
|
|
184
|
+
description = COALESCE(EXCLUDED.description, auth_rbac.auth_group.description),
|
|
185
|
+
modified = NOW()
|
|
186
|
+
"""),
|
|
187
|
+
{"creator": self.client, "role": role, "description": description},
|
|
189
188
|
)
|
|
190
|
-
self.db.add(new_group)
|
|
191
189
|
self.db.commit()
|
|
190
|
+
return True
|
|
192
191
|
except Exception:
|
|
193
192
|
self.db.rollback()
|
|
194
|
-
|
|
195
|
-
self.db.expunge_all()
|
|
196
|
-
# Check if the role was created by another transaction
|
|
197
|
-
existing = (
|
|
198
|
-
self.db.query(AuthGroup)
|
|
199
|
-
.filter(AuthGroup.creator == self.client, AuthGroup.role == role)
|
|
200
|
-
.first()
|
|
201
|
-
)
|
|
202
|
-
if existing:
|
|
203
|
-
return True # Role already exists
|
|
204
|
-
return False # Failed to create role
|
|
205
|
-
|
|
206
|
-
return True
|
|
193
|
+
return False
|
|
207
194
|
|
|
208
195
|
def del_role(self, role: str) -> bool:
|
|
209
196
|
"""Delete a role"""
|
|
@@ -220,54 +207,57 @@ class AuthorizationService:
|
|
|
220
207
|
return False
|
|
221
208
|
|
|
222
209
|
def add_membership(self, user: str, role: str) -> bool:
|
|
223
|
-
"""Add user to a role
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
210
|
+
"""Add user to a role - atomic idempotent using ON CONFLICT.
|
|
211
|
+
|
|
212
|
+
Uses PostgreSQL's INSERT ... ON CONFLICT for race-condition-free operations.
|
|
213
|
+
"""
|
|
214
|
+
from sqlalchemy import text
|
|
215
|
+
|
|
216
|
+
# Get group ID
|
|
217
|
+
group_result = self.db.execute(
|
|
218
|
+
text("""
|
|
219
|
+
SELECT id FROM auth_rbac.auth_group
|
|
220
|
+
WHERE creator = :creator AND role = :role AND is_active = true
|
|
221
|
+
"""),
|
|
222
|
+
{"creator": self.client, "role": role},
|
|
232
223
|
)
|
|
233
|
-
|
|
234
|
-
if not
|
|
224
|
+
row = group_result.fetchone()
|
|
225
|
+
if not row:
|
|
235
226
|
return False
|
|
227
|
+
group_id = row[0]
|
|
236
228
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
self.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
# Check if user is already in the group
|
|
266
|
-
if group not in membership.groups:
|
|
267
|
-
membership.groups.append(group)
|
|
229
|
+
try:
|
|
230
|
+
# Upsert membership
|
|
231
|
+
encrypted_user = self._get_encrypted_user(user)
|
|
232
|
+
membership_result = self.db.execute(
|
|
233
|
+
text("""
|
|
234
|
+
INSERT INTO auth_rbac.auth_membership
|
|
235
|
+
(creator, "user", is_active, date_created, modified)
|
|
236
|
+
VALUES
|
|
237
|
+
(:creator, :user, true, NOW(), NOW())
|
|
238
|
+
ON CONFLICT (creator, "user") DO UPDATE SET
|
|
239
|
+
is_active = true,
|
|
240
|
+
modified = NOW()
|
|
241
|
+
RETURNING id
|
|
242
|
+
"""),
|
|
243
|
+
{"creator": self.client, "user": encrypted_user},
|
|
244
|
+
)
|
|
245
|
+
membership_id = membership_result.fetchone()[0]
|
|
246
|
+
|
|
247
|
+
# Link membership to group (junction table)
|
|
248
|
+
self.db.execute(
|
|
249
|
+
text("""
|
|
250
|
+
INSERT INTO auth_rbac.membership_groups (membership_id, group_id)
|
|
251
|
+
VALUES (:membership_id, :group_id)
|
|
252
|
+
ON CONFLICT (membership_id, group_id) DO NOTHING
|
|
253
|
+
"""),
|
|
254
|
+
{"membership_id": membership_id, "group_id": group_id},
|
|
255
|
+
)
|
|
268
256
|
self.db.commit()
|
|
269
|
-
|
|
270
|
-
|
|
257
|
+
return True
|
|
258
|
+
except Exception:
|
|
259
|
+
self.db.rollback()
|
|
260
|
+
return False
|
|
271
261
|
|
|
272
262
|
def del_membership(self, user: str, role: str) -> bool:
|
|
273
263
|
"""Remove user from a role"""
|
|
@@ -318,60 +308,60 @@ class AuthorizationService:
|
|
|
318
308
|
)
|
|
319
309
|
|
|
320
310
|
def add_permission(self, role: str, name: str) -> bool:
|
|
321
|
-
"""Add permission to a role
|
|
311
|
+
"""Add permission to a role - atomic idempotent using ON CONFLICT.
|
|
312
|
+
|
|
313
|
+
Uses PostgreSQL's INSERT ... ON CONFLICT for race-condition-free operations.
|
|
314
|
+
"""
|
|
315
|
+
from sqlalchemy import text
|
|
316
|
+
|
|
322
317
|
if self.has_permission(role, name):
|
|
323
318
|
return True
|
|
324
319
|
|
|
325
|
-
group
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
.first()
|
|
320
|
+
# Get group ID
|
|
321
|
+
group_result = self.db.execute(
|
|
322
|
+
text("""
|
|
323
|
+
SELECT id FROM auth_rbac.auth_group
|
|
324
|
+
WHERE creator = :creator AND role = :role AND is_active = true
|
|
325
|
+
"""),
|
|
326
|
+
{"creator": self.client, "role": role},
|
|
333
327
|
)
|
|
334
|
-
|
|
335
|
-
if not
|
|
328
|
+
row = group_result.fetchone()
|
|
329
|
+
if not row:
|
|
336
330
|
return False
|
|
331
|
+
group_id = row[0]
|
|
337
332
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
self.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
# If it's still None after rollback, something else went wrong
|
|
366
|
-
if not permission:
|
|
367
|
-
return False
|
|
368
|
-
|
|
369
|
-
# Check if permission is already in the group
|
|
370
|
-
if group not in permission.groups:
|
|
371
|
-
permission.groups.append(group)
|
|
333
|
+
try:
|
|
334
|
+
# Upsert permission
|
|
335
|
+
encrypted_name = self._get_encrypted_permission(name)
|
|
336
|
+
perm_result = self.db.execute(
|
|
337
|
+
text("""
|
|
338
|
+
INSERT INTO auth_rbac.auth_permission
|
|
339
|
+
(creator, name, is_active, date_created, modified)
|
|
340
|
+
VALUES
|
|
341
|
+
(:creator, :name, true, NOW(), NOW())
|
|
342
|
+
ON CONFLICT (creator, name) DO UPDATE SET
|
|
343
|
+
is_active = true,
|
|
344
|
+
modified = NOW()
|
|
345
|
+
RETURNING id
|
|
346
|
+
"""),
|
|
347
|
+
{"creator": self.client, "name": encrypted_name},
|
|
348
|
+
)
|
|
349
|
+
perm_id = perm_result.fetchone()[0]
|
|
350
|
+
|
|
351
|
+
# Link permission to group (junction table)
|
|
352
|
+
self.db.execute(
|
|
353
|
+
text("""
|
|
354
|
+
INSERT INTO auth_rbac.permission_groups (permission_id, group_id)
|
|
355
|
+
VALUES (:perm_id, :group_id)
|
|
356
|
+
ON CONFLICT (permission_id, group_id) DO NOTHING
|
|
357
|
+
"""),
|
|
358
|
+
{"perm_id": perm_id, "group_id": group_id},
|
|
359
|
+
)
|
|
372
360
|
self.db.commit()
|
|
373
|
-
|
|
374
|
-
|
|
361
|
+
return True
|
|
362
|
+
except Exception:
|
|
363
|
+
self.db.rollback()
|
|
364
|
+
return False
|
|
375
365
|
|
|
376
366
|
def del_permission(self, role: str, name: str) -> bool:
|
|
377
367
|
"""Remove permission from a role"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: auth
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Authorization for humans
|
|
5
5
|
Author-email: Farshid Ashouri <farsheed.ashouri@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -33,6 +33,8 @@ Requires-Dist: PyJWT>=2.0.0
|
|
|
33
33
|
Requires-Dist: cryptography>=3.0.0
|
|
34
34
|
Requires-Dist: APScheduler>=3.0.0
|
|
35
35
|
Requires-Dist: psycopg[binary]>=3.0.0
|
|
36
|
+
Requires-Dist: pydantic>=2.0.0
|
|
37
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
36
38
|
Provides-Extra: dev
|
|
37
39
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
38
40
|
Requires-Dist: pytest-cov>=2.0; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "auth"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.0"
|
|
8
8
|
description = "Authorization for humans"
|
|
9
9
|
readme = {file = "README.rst", content-type = "text/x-rst"}
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -41,6 +41,8 @@ dependencies = [
|
|
|
41
41
|
"cryptography>=3.0.0",
|
|
42
42
|
"APScheduler>=3.0.0",
|
|
43
43
|
"psycopg[binary]>=3.0.0",
|
|
44
|
+
"pydantic>=2.0.0",
|
|
45
|
+
"pydantic-settings>=2.0.0",
|
|
44
46
|
]
|
|
45
47
|
|
|
46
48
|
[project.optional-dependencies]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|