authweave-workload 7.0.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.
Files changed (34) hide show
  1. authweave_workload-7.0.0/.gitignore +186 -0
  2. authweave_workload-7.0.0/LICENSE +21 -0
  3. authweave_workload-7.0.0/PKG-INFO +70 -0
  4. authweave_workload-7.0.0/README.md +24 -0
  5. authweave_workload-7.0.0/authweave_workload/__init__.py +51 -0
  6. authweave_workload-7.0.0/authweave_workload/delegation.py +76 -0
  7. authweave_workload-7.0.0/authweave_workload/events.py +70 -0
  8. authweave_workload-7.0.0/authweave_workload/integrations/__init__.py +3 -0
  9. authweave_workload-7.0.0/authweave_workload/integrations/litestar.py +375 -0
  10. authweave_workload-7.0.0/authweave_workload/jwks.py +215 -0
  11. authweave_workload-7.0.0/authweave_workload/jwt.py +379 -0
  12. authweave_workload-7.0.0/authweave_workload/lifecycle.py +344 -0
  13. authweave_workload-7.0.0/authweave_workload/migrations/0001_postgresql.sql +66 -0
  14. authweave_workload-7.0.0/authweave_workload/models.py +246 -0
  15. authweave_workload-7.0.0/authweave_workload/mtls.py +167 -0
  16. authweave_workload-7.0.0/authweave_workload/provider.py +222 -0
  17. authweave_workload-7.0.0/authweave_workload/py.typed +0 -0
  18. authweave_workload-7.0.0/authweave_workload/rate_limit.py +41 -0
  19. authweave_workload-7.0.0/authweave_workload/sqlalchemy.py +483 -0
  20. authweave_workload-7.0.0/authweave_workload/stores.py +96 -0
  21. authweave_workload-7.0.0/pyproject.toml +71 -0
  22. authweave_workload-7.0.0/tests/__init__.py +1 -0
  23. authweave_workload-7.0.0/tests/test_delegation.py +78 -0
  24. authweave_workload-7.0.0/tests/test_direct_mtls_negative_matrix.py +248 -0
  25. authweave_workload-7.0.0/tests/test_imports.py +17 -0
  26. authweave_workload-7.0.0/tests/test_jwks_matrix.py +234 -0
  27. authweave_workload-7.0.0/tests/test_jwt_negative_matrix.py +439 -0
  28. authweave_workload-7.0.0/tests/test_jwt_provider.py +171 -0
  29. authweave_workload-7.0.0/tests/test_lifecycle_negative_matrix.py +507 -0
  30. authweave_workload-7.0.0/tests/test_lifecycle_provider.py +328 -0
  31. authweave_workload-7.0.0/tests/test_litestar_integration.py +289 -0
  32. authweave_workload-7.0.0/tests/test_mtls.py +307 -0
  33. authweave_workload-7.0.0/tests/test_sqlalchemy_matrix.py +237 -0
  34. authweave_workload-7.0.0/tests/test_sqlalchemy_store.py +84 -0
@@ -0,0 +1,186 @@
1
+ .gitignore### Python ###
2
+ # Byte-compiled / optimized / DLL files
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+
7
+ # C extensions
8
+ *.so
9
+
10
+ # Distribution / packaging
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+ *.db
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # poetry
100
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
104
+ #poetry.lock
105
+
106
+ # pdm
107
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
108
+ #pdm.lock
109
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
110
+ # in version control.
111
+ # https://pdm.fming.dev/#use-with-ide
112
+ .pdm.toml
113
+
114
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115
+ __pypackages__/
116
+
117
+ # Celery stuff
118
+ celerybeat-schedule
119
+ celerybeat.pid
120
+
121
+ # SageMath parsed files
122
+ *.sage.py
123
+
124
+ # Environments
125
+ .venv
126
+ env/
127
+ venv/
128
+ ENV/
129
+ env.bak/
130
+ venv.bak/
131
+
132
+ # Spyder project settings
133
+ .spyderproject
134
+ .spyproject
135
+
136
+ # Rope project settings
137
+ .ropeproject
138
+
139
+ # zensical documentation
140
+ /site
141
+ docs/_include/
142
+
143
+ # mypy
144
+ .mypy_cache/
145
+ .dmypy.json
146
+ dmypy.json
147
+
148
+ # Pyre type checker
149
+ .pyre/
150
+
151
+ # pytype static type analyzer
152
+ .pytype/
153
+
154
+ # Cython debug symbols
155
+ cython_debug/
156
+
157
+ # PyCharm
158
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
161
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162
+ #.idea/
163
+
164
+ ### Python Patch ###
165
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
166
+ poetry.toml
167
+
168
+ # ruff
169
+ .ruff_cache/
170
+
171
+ # LSP config files
172
+ pyrightconfig.json
173
+
174
+ .env
175
+ .uv-cache
176
+
177
+ # AI agents
178
+ PRD.md
179
+ AGENTS.md
180
+ CLAUDE.md
181
+ TECHSTACK.md
182
+ .agents/
183
+ .claude/
184
+ .cursor/
185
+ .codacy/
186
+ .codegraph/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vladislav Shepilov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: authweave-workload
3
+ Version: 7.0.0
4
+ Summary: Framework-neutral sender-constrained workload authentication
5
+ Project-URL: homepage, https://github.com/ZYLVEXT/litestar-auth
6
+ Project-URL: documentation, https://zylvext.github.io/litestar-auth/
7
+ Project-URL: source, https://github.com/ZYLVEXT/litestar-auth
8
+ Project-URL: tracker, https://github.com/ZYLVEXT/litestar-auth/issues
9
+ Author-email: Vladislav Shepilov <shepilov.v@protonmail.com>
10
+ Maintainer-email: Vladislav Shepilov <shepilov.v@protonmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: authentication,mtls,oauth,workload,x509
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Security
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: <3.15.0,>=3.12.0
23
+ Requires-Dist: authweave-core<8.0,>=7.0.0
24
+ Provides-Extra: all
25
+ Requires-Dist: anyio<5.0,>=4.14.2; extra == 'all'
26
+ Requires-Dist: cryptography>=49.0.0; extra == 'all'
27
+ Requires-Dist: httpx<1.0,>=0.28.1; extra == 'all'
28
+ Requires-Dist: litestar-auth<8.0,>=7.0.0; extra == 'all'
29
+ Requires-Dist: litestar<3.0,>=2.22.0; extra == 'all'
30
+ Requires-Dist: pyjwt<3.0,>=2.13.0; extra == 'all'
31
+ Requires-Dist: sqlalchemy<3.0,>=2.0.48; extra == 'all'
32
+ Provides-Extra: jwt
33
+ Requires-Dist: anyio<5.0,>=4.14.2; extra == 'jwt'
34
+ Requires-Dist: cryptography>=49.0.0; extra == 'jwt'
35
+ Requires-Dist: httpx<1.0,>=0.28.1; extra == 'jwt'
36
+ Requires-Dist: pyjwt<3.0,>=2.13.0; extra == 'jwt'
37
+ Provides-Extra: litestar
38
+ Requires-Dist: litestar-auth<8.0,>=7.0.0; extra == 'litestar'
39
+ Requires-Dist: litestar<3.0,>=2.22.0; extra == 'litestar'
40
+ Requires-Dist: sqlalchemy<3.0,>=2.0.48; extra == 'litestar'
41
+ Provides-Extra: mtls
42
+ Requires-Dist: cryptography>=49.0.0; extra == 'mtls'
43
+ Provides-Extra: sqlalchemy
44
+ Requires-Dist: sqlalchemy<3.0,>=2.0.48; extra == 'sqlalchemy'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # authweave-workload
48
+
49
+ Framework-neutral authentication for registered service applications and
50
+ service, workload, or agent principals. Version 7 supports registered X.509
51
+ credentials, direct mTLS, and mTLS-bound access tokens issued by an external
52
+ OAuth Authorization Server.
53
+
54
+ ```bash
55
+ uv add 'authweave-workload[mtls,jwt]'
56
+ ```
57
+
58
+ The base package never accepts or stores private keys and imports no web
59
+ framework, ORM, or cryptography implementation.
60
+
61
+ Lifecycle mutations require a verified actor, a correlation ID, and an application recorder that
62
+ writes the security event in the same transaction. X.509 registration accepts only opaque
63
+ `CertificateMetadata` returned by `validate_public_certificate`; it cannot be built from unchecked
64
+ fields.
65
+
66
+ The optional Litestar adapter accepts Envoy evidence only over an explicitly
67
+ trusted TCP peer or permission-restricted Unix socket. Envoy's hex certificate
68
+ fingerprint is normalized to canonical base64url, while revocation freshness
69
+ comes from application-trusted CRL/control-plane metadata rather than an
70
+ inbound request header.
@@ -0,0 +1,24 @@
1
+ # authweave-workload
2
+
3
+ Framework-neutral authentication for registered service applications and
4
+ service, workload, or agent principals. Version 7 supports registered X.509
5
+ credentials, direct mTLS, and mTLS-bound access tokens issued by an external
6
+ OAuth Authorization Server.
7
+
8
+ ```bash
9
+ uv add 'authweave-workload[mtls,jwt]'
10
+ ```
11
+
12
+ The base package never accepts or stores private keys and imports no web
13
+ framework, ORM, or cryptography implementation.
14
+
15
+ Lifecycle mutations require a verified actor, a correlation ID, and an application recorder that
16
+ writes the security event in the same transaction. X.509 registration accepts only opaque
17
+ `CertificateMetadata` returned by `validate_public_certificate`; it cannot be built from unchecked
18
+ fields.
19
+
20
+ The optional Litestar adapter accepts Envoy evidence only over an explicitly
21
+ trusted TCP peer or permission-restricted Unix socket. Envoy's hex certificate
22
+ fingerprint is normalized to canonical base64url, while revocation freshness
23
+ comes from application-trusted CRL/control-plane metadata rather than an
24
+ inbound request header.
@@ -0,0 +1,51 @@
1
+ """Framework-neutral workload authentication."""
2
+
3
+ from authweave_workload.events import SecurityEvent, SecurityEventType
4
+ from authweave_workload.lifecycle import EventRecorder, LifecycleConflictError, WorkloadLifecycleService
5
+ from authweave_workload.models import (
6
+ CertificateMetadata,
7
+ CredentialStatus,
8
+ EntityStatus,
9
+ MachineCredential,
10
+ MachinePrincipal,
11
+ ResolvedMachineIdentity,
12
+ ServiceApplication,
13
+ )
14
+ from authweave_workload.provider import DirectMTLSPolicy, DirectMTLSProvider
15
+ from authweave_workload.rate_limit import WorkloadRateLimitIdentity, rate_limit_identity
16
+ from authweave_workload.stores import (
17
+ MachineCredentialStore,
18
+ MachinePrincipalStore,
19
+ ServiceApplicationStore,
20
+ StoreConflictError,
21
+ StoreUnavailableError,
22
+ WorkloadStore,
23
+ )
24
+
25
+ __version__ = "7.0.0"
26
+
27
+ __all__ = (
28
+ "CertificateMetadata",
29
+ "CredentialStatus",
30
+ "DirectMTLSPolicy",
31
+ "DirectMTLSProvider",
32
+ "EntityStatus",
33
+ "EventRecorder",
34
+ "LifecycleConflictError",
35
+ "MachineCredential",
36
+ "MachineCredentialStore",
37
+ "MachinePrincipal",
38
+ "MachinePrincipalStore",
39
+ "ResolvedMachineIdentity",
40
+ "SecurityEvent",
41
+ "SecurityEventType",
42
+ "ServiceApplication",
43
+ "ServiceApplicationStore",
44
+ "StoreConflictError",
45
+ "StoreUnavailableError",
46
+ "WorkloadLifecycleService",
47
+ "WorkloadRateLimitIdentity",
48
+ "WorkloadStore",
49
+ "__version__",
50
+ "rate_limit_identity",
51
+ )
@@ -0,0 +1,76 @@
1
+ """Trusted RFC 8693 actor-claim mapping with bounded delegation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+
8
+ from authweave_core import FailureCode, PrincipalRef
9
+
10
+ _MAX_SCOPE_VALUES = 64
11
+ _ACTOR_KEYS = frozenset({"act", "client_id", "scope", "sub"})
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class Delegation:
16
+ """Verified actor and bounded actor chain derived from a trusted token."""
17
+
18
+ actor: PrincipalRef
19
+ chain: tuple[PrincipalRef, ...]
20
+ effective_scopes: tuple[str, ...]
21
+
22
+
23
+ def map_rfc8693_actor(
24
+ claim: object,
25
+ *,
26
+ issuer: str,
27
+ subject: PrincipalRef,
28
+ actor_kind: str,
29
+ credential_scopes: tuple[str, ...],
30
+ maximum_depth: int,
31
+ ) -> Delegation | FailureCode:
32
+ """Map a signed actor claim without deriving authority from metadata or kind."""
33
+ if maximum_depth < 1:
34
+ return FailureCode.INTERNAL_INVARIANT
35
+ if not isinstance(claim, Mapping):
36
+ return FailureCode.INVALID
37
+ chain: list[PrincipalRef] = []
38
+ seen = {(subject.issuer, subject.subject)}
39
+ effective_scopes = set(credential_scopes)
40
+ current: object = claim
41
+ while current is not None:
42
+ if len(chain) >= maximum_depth or not isinstance(current, Mapping) or not set(current) <= _ACTOR_KEYS:
43
+ return FailureCode.INVALID
44
+ actor_subject = current.get("sub")
45
+ if not isinstance(actor_subject, str) or not actor_subject:
46
+ return FailureCode.INVALID
47
+ identity = (issuer, actor_subject)
48
+ if identity in seen:
49
+ return FailureCode.INVALID
50
+ seen.add(identity)
51
+ try:
52
+ actor = PrincipalRef(issuer, actor_subject, actor_kind)
53
+ except ValueError:
54
+ return FailureCode.INVALID
55
+ chain.append(actor)
56
+ actor_scopes = current.get("scope")
57
+ if actor_scopes is not None:
58
+ if not isinstance(actor_scopes, str):
59
+ return FailureCode.INVALID
60
+ parsed_scopes = tuple(actor_scopes.split())
61
+ if (
62
+ len(parsed_scopes) > _MAX_SCOPE_VALUES
63
+ or len(parsed_scopes) != len(set(parsed_scopes))
64
+ or any("*" in value for value in parsed_scopes)
65
+ ):
66
+ return FailureCode.INVALID
67
+ effective_scopes.intersection_update(parsed_scopes)
68
+ current = current.get("act")
69
+ return Delegation(
70
+ actor=chain[0],
71
+ chain=tuple(chain),
72
+ effective_scopes=tuple(scope for scope in credential_scopes if scope in effective_scopes),
73
+ )
74
+
75
+
76
+ __all__ = ("Delegation", "map_rfc8693_actor")
@@ -0,0 +1,70 @@
1
+ """Typed secret-free workload security events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import UTC, datetime
7
+ from enum import StrEnum
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from authweave_core import FailureCode, PrincipalRef
12
+
13
+ _MAX_CORRELATION_ID_LENGTH = 512
14
+
15
+
16
+ class SecurityEventType(StrEnum):
17
+ """Stable event classes emitted by workload lifecycle and providers."""
18
+
19
+ APPLICATION_CREATED = "application_created"
20
+ APPLICATION_ENABLED = "application_enabled"
21
+ APPLICATION_DISABLED = "application_disabled"
22
+ APPLICATION_METADATA_UPDATED = "application_metadata_updated"
23
+ PRINCIPAL_CREATED = "principal_created"
24
+ PRINCIPAL_ENABLED = "principal_enabled"
25
+ PRINCIPAL_DISABLED = "principal_disabled"
26
+ PRINCIPAL_METADATA_UPDATED = "principal_metadata_updated"
27
+ CREDENTIAL_REGISTERED = "credential_registered"
28
+ CREDENTIAL_ROTATION_STARTED = "credential_rotation_started"
29
+ CREDENTIAL_ROTATION_COMPLETED = "credential_rotation_completed"
30
+ CREDENTIAL_REVOKED = "credential_revoked"
31
+ AUTHENTICATION_SUCCEEDED = "authentication_succeeded"
32
+ AUTHENTICATION_FAILED = "authentication_failed"
33
+ PROVIDER_UNAVAILABLE = "provider_unavailable"
34
+ SENDER_CONSTRAINT_REJECTED = "sender_constraint_rejected"
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class SecurityEvent:
39
+ """One bounded event with no raw credential or certificate body."""
40
+
41
+ type: SecurityEventType
42
+ target_application_id: str | None = None
43
+ target_principal: PrincipalRef | None = None
44
+ credential_id: str | None = None
45
+ actor: PrincipalRef | None = None
46
+ provider: str | None = None
47
+ profile: str | None = None
48
+ reason: FailureCode | None = None
49
+ correlation_id: str | None = None
50
+ timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
51
+
52
+ def __post_init__(self) -> None:
53
+ """Require an aware timestamp.
54
+
55
+ Raises:
56
+ ValueError: If the timestamp has no timezone.
57
+ """
58
+ if self.timestamp.utcoffset() is None:
59
+ msg = "event timestamp must be timezone-aware"
60
+ raise ValueError(msg)
61
+ if self.correlation_id is not None and (
62
+ not self.correlation_id
63
+ or self.correlation_id != self.correlation_id.strip()
64
+ or len(self.correlation_id) > _MAX_CORRELATION_ID_LENGTH
65
+ ):
66
+ msg = "event correlation_id must be non-empty, trimmed, and at most 512 characters"
67
+ raise ValueError(msg)
68
+
69
+
70
+ __all__ = ("SecurityEvent", "SecurityEventType")
@@ -0,0 +1,3 @@
1
+ """Optional framework integrations for authweave-workload."""
2
+
3
+ __all__: tuple[str, ...] = ()