alpha-python 0.2.5__tar.gz → 0.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.
- {alpha_python-0.2.5 → alpha_python-0.3.0}/PKG-INFO +1 -1
- {alpha_python-0.2.5 → alpha_python-0.3.0}/pyproject.toml +1 -1
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/__init__.py +9 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/controller.mustache +2 -2
- alpha_python-0.3.0/src/alpha/infra/connectors/__init__.py +11 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/infra/connectors/ldap_connector.py +1 -1
- alpha_python-0.3.0/src/alpha/infra/connectors/oidc_connector.py +503 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/providers/__init__.py +3 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/providers/ldap_provider.py +43 -11
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/providers/models/identity.py +9 -6
- alpha_python-0.3.0/src/alpha/providers/oidc_provider.py +418 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha_python.egg-info/PKG-INFO +1 -1
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha_python.egg-info/SOURCES.txt +2 -1
- alpha_python-0.2.5/src/alpha/infra/connectors/__init__.py +0 -5
- alpha_python-0.2.5/src/alpha/providers/keycloak_provider.py +0 -16
- {alpha_python-0.2.5 → alpha_python-0.3.0}/LICENSE +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/README.md +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/setup.cfg +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/adapters/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/adapters/sqla_unit_of_work.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/cli.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/containers/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/containers/container.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/domain/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/domain/models/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/domain/models/base_model.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/domain/models/life_cycle_base.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/domain/models/user.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/encoder.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/exceptions.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/_type_conversion_matrix.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/_type_mapping.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/class_factories.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/default_field_factory.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/field_iterator.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/jwt_factory.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/logging_handler_factory.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/model_class_factory.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/models/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/models/factory_classes.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/request_factory.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/response_factory.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/factories/type_factories.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/api_generate_handler.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/api_run_handler.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/base_handler.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/gen-code.sh +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/models/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/models/argument.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/models/command.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/models/section.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/models/subparser.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/run-api.sh +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/Dockerfile.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/README.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/__init__model.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/__init__test.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/__main__.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/base_model.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/controller_test.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/dockerignore.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/encoder.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/git_push.sh.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/gitignore.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/model.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/openapi.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/param_type.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/requirements.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/security_controller_.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/setup.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/test-requirements.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/tox.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/travis.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/typing_utils.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/handlers/templates/python-flask/util.mustache +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/infra/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/infra/databases/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/infra/databases/sql_alchemy.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/infra/models/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/infra/models/filter_operators.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/infra/models/json_patch.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/infra/models/order_by.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/infra/models/query_clause.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/infra/models/search_filter.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/attrs_instance.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/dataclass_instance.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/factories.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/handler.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/openapi_model.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/patchable.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/providers.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/pydantic_instance.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/sql_database.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/sql_mapper.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/sql_repository.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/token_factory.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/unit_of_work.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/interfaces/updateable.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/mixins/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/mixins/jwt_provider.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/providers/api_key_provider.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/providers/database_provider.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/providers/local_provider.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/providers/models/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/providers/models/credentials.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/providers/models/token.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/py.typed +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/repositories/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/repositories/models/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/repositories/models/repository_model.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/repositories/sql_alchemy_repository.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/services/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/services/authentication_service.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/utils/__init__.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/utils/_http_codes.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/utils/is_attrs.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/utils/is_pydantic.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/utils/logging_configurator.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/utils/logging_level_checker.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/utils/response_object.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/utils/verify_identity.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha/utils/version_checker.py +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha_python.egg-info/dependency_links.txt +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha_python.egg-info/entry_points.txt +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha_python.egg-info/requires.txt +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/src/alpha_python.egg-info/top_level.txt +0 -0
- {alpha_python-0.2.5 → alpha_python-0.3.0}/tests/test_encoder.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpha-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Alpha is intended to be the first dependency you need to add to your Python application. It is a Python library which contains standard building blocks that can be used in applications that are used as APIs and/or make use of database interaction.
|
|
5
5
|
Author-email: Bart Reijling <bart@reijling.eu>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "alpha-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Alpha is intended to be the first dependency you need to add to your Python application. It is a Python library which contains standard building blocks that can be used in applications that are used as APIs and/or make use of database interaction."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -6,6 +6,10 @@ from alpha.domain.models.user import User
|
|
|
6
6
|
from alpha.domain.models.base_model import BaseDomainModel, DomainModel
|
|
7
7
|
from alpha.domain.models.life_cycle_base import LifeCycleBase
|
|
8
8
|
from alpha.infra.connectors.ldap_connector import LDAPConnector
|
|
9
|
+
from alpha.infra.connectors.oidc_connector import (
|
|
10
|
+
OIDCConnector,
|
|
11
|
+
KeyCloakOIDCConnector,
|
|
12
|
+
)
|
|
9
13
|
from alpha.infra.databases.sql_alchemy import SqlAlchemyDatabase
|
|
10
14
|
from alpha.infra.models.filter_operators import And, Or
|
|
11
15
|
from alpha.infra.models.json_patch import JsonPatch
|
|
@@ -40,6 +44,7 @@ from alpha.providers.models.identity import (
|
|
|
40
44
|
from alpha.providers.models.credentials import PasswordCredentials
|
|
41
45
|
from alpha.providers.models.token import Token
|
|
42
46
|
from alpha.providers.ldap_provider import LDAPProvider, ADProvider
|
|
47
|
+
from alpha.providers.oidc_provider import OIDCProvider, KeyCloakProvider
|
|
43
48
|
from alpha.repositories.models.repository_model import RepositoryModel
|
|
44
49
|
from alpha.repositories.sql_alchemy_repository import SqlAlchemyRepository
|
|
45
50
|
from alpha.services.authentication_service import AuthenticationService
|
|
@@ -67,6 +72,8 @@ __all__ = [
|
|
|
67
72
|
"LifeCycleBase",
|
|
68
73
|
"User",
|
|
69
74
|
"LDAPConnector",
|
|
75
|
+
"OIDCConnector",
|
|
76
|
+
"KeyCloakOIDCConnector",
|
|
70
77
|
"SqlAlchemyDatabase",
|
|
71
78
|
"And",
|
|
72
79
|
"Or",
|
|
@@ -101,6 +108,8 @@ __all__ = [
|
|
|
101
108
|
"Token",
|
|
102
109
|
"LDAPProvider",
|
|
103
110
|
"ADProvider",
|
|
111
|
+
"OIDCProvider",
|
|
112
|
+
"KeyCloakProvider",
|
|
104
113
|
"RepositoryModel",
|
|
105
114
|
"SqlAlchemyRepository",
|
|
106
115
|
"AuthenticationService",
|
|
@@ -154,8 +154,8 @@ def {{operationId}}(
|
|
|
154
154
|
try:
|
|
155
155
|
# Objects used for authorization
|
|
156
156
|
roles=[{{#vendorExtensions.x-alpha-verify-roles}}"{{.}}",{{/vendorExtensions.x-alpha-verify-roles}}]
|
|
157
|
-
groups=[{{#vendorExtensions.x-alpha-verify-groups}}"{{.}}",{{/vendorExtensions.x-alpha-verify-groups}}]
|
|
158
|
-
permissions=[{{#vendorExtensions.x-alpha-verify-permissions}}"{{.}}",{{/vendorExtensions.x-alpha-verify-permissions}}]
|
|
157
|
+
groups=[{{#vendorExtensions.x-alpha-verify-groups}}"{{.}}",{{/vendorExtensions.x-alpha-verify-groups}}]
|
|
158
|
+
permissions=[{{#vendorExtensions.x-alpha-verify-permissions}}"{{.}}",{{/vendorExtensions.x-alpha-verify-permissions}}]
|
|
159
159
|
{{#authMethods}}{{#isBasicBearer}}
|
|
160
160
|
# validate token
|
|
161
161
|
if token_factory.validate(token):
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""OIDC connectors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, cast
|
|
6
|
+
from urllib.parse import quote, urljoin
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from requests.auth import HTTPBasicAuth
|
|
10
|
+
|
|
11
|
+
from alpha import exceptions
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OIDCConnector:
|
|
15
|
+
"""OIDC connector for interacting with an OpenID Connect identity provider
|
|
16
|
+
via OIDC/OAuth2 protocols.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
token_url
|
|
21
|
+
Token endpoint URL or relative path.
|
|
22
|
+
userinfo_url
|
|
23
|
+
Optional userinfo endpoint URL or relative path.
|
|
24
|
+
introspection_url
|
|
25
|
+
Optional token introspection endpoint URL or relative path.
|
|
26
|
+
client_id
|
|
27
|
+
OAuth2 client identifier used for token requests.
|
|
28
|
+
client_secret
|
|
29
|
+
OAuth2 client secret used for token requests.
|
|
30
|
+
scope
|
|
31
|
+
Space-delimited OAuth2 scopes for standard token requests.
|
|
32
|
+
verify_tls
|
|
33
|
+
Whether to verify TLS certificates for HTTP requests.
|
|
34
|
+
timeout_seconds
|
|
35
|
+
Request timeout in seconds.
|
|
36
|
+
use_basic_auth
|
|
37
|
+
Whether to send client credentials via HTTP Basic Auth.
|
|
38
|
+
base_url
|
|
39
|
+
Optional base URL to resolve relative endpoints.
|
|
40
|
+
user_lookup_url_template
|
|
41
|
+
Template URL used to look up a user by subject/username.
|
|
42
|
+
admin_client_id
|
|
43
|
+
Optional client identifier for admin token requests.
|
|
44
|
+
admin_client_secret
|
|
45
|
+
Optional client secret for admin token requests.
|
|
46
|
+
admin_scope
|
|
47
|
+
Optional scope override for admin token requests.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
token_url: str,
|
|
53
|
+
userinfo_url: str | None = None,
|
|
54
|
+
introspection_url: str | None = None,
|
|
55
|
+
client_id: str | None = None,
|
|
56
|
+
client_secret: str | None = None,
|
|
57
|
+
scope: str | list[str] | None = None,
|
|
58
|
+
verify_tls: bool = True,
|
|
59
|
+
timeout_seconds: int = 10,
|
|
60
|
+
use_basic_auth: bool = True,
|
|
61
|
+
base_url: str | None = None,
|
|
62
|
+
user_lookup_url_template: str | None = None,
|
|
63
|
+
admin_client_id: str | None = None,
|
|
64
|
+
admin_client_secret: str | None = None,
|
|
65
|
+
admin_scope: str | list[str] | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
self._base_url = base_url
|
|
68
|
+
self._token_url = self._build_url(token_url)
|
|
69
|
+
self._userinfo_url = (
|
|
70
|
+
self._build_url(userinfo_url) if userinfo_url else None
|
|
71
|
+
)
|
|
72
|
+
self._introspection_url = (
|
|
73
|
+
self._build_url(introspection_url) if introspection_url else None
|
|
74
|
+
)
|
|
75
|
+
self._user_lookup_url_template = user_lookup_url_template
|
|
76
|
+
self._client_id = client_id
|
|
77
|
+
self._client_secret = client_secret
|
|
78
|
+
self._scope = self._sanitize_scope(scope)
|
|
79
|
+
self._verify_tls = verify_tls
|
|
80
|
+
self._timeout_seconds = timeout_seconds
|
|
81
|
+
self._use_basic_auth = use_basic_auth
|
|
82
|
+
self._admin_client_id = admin_client_id or client_id
|
|
83
|
+
self._admin_client_secret = admin_client_secret or client_secret
|
|
84
|
+
self._admin_scope = self._sanitize_scope(admin_scope) or self._scope
|
|
85
|
+
|
|
86
|
+
self._session = requests.Session()
|
|
87
|
+
|
|
88
|
+
def close(self) -> None:
|
|
89
|
+
"""Close the underlying HTTP session to release resources."""
|
|
90
|
+
self._session.close()
|
|
91
|
+
|
|
92
|
+
def __enter__(self) -> "OIDCConnector":
|
|
93
|
+
"""Enter the runtime context related to this object."""
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
97
|
+
"""Exit the runtime context and close the HTTP session."""
|
|
98
|
+
self.close()
|
|
99
|
+
@property
|
|
100
|
+
def userinfo_url(self) -> str | None:
|
|
101
|
+
"""Return the configured userinfo endpoint URL.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
The full userinfo endpoint URL or None.
|
|
106
|
+
"""
|
|
107
|
+
return self._userinfo_url
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def introspection_url(self) -> str | None:
|
|
111
|
+
"""Return the configured introspection endpoint URL.
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
The full introspection endpoint URL or None.
|
|
116
|
+
"""
|
|
117
|
+
return self._introspection_url
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def user_lookup_url_template(self) -> str | None:
|
|
121
|
+
"""Return the configured user lookup URL template.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
URL template for user lookup or None.
|
|
126
|
+
"""
|
|
127
|
+
return self._user_lookup_url_template
|
|
128
|
+
|
|
129
|
+
def request_password_token(
|
|
130
|
+
self, username: str, password: str
|
|
131
|
+
) -> dict[str, Any]:
|
|
132
|
+
"""Request an access token using the password grant.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
username
|
|
137
|
+
Resource owner username.
|
|
138
|
+
password
|
|
139
|
+
Resource owner password.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
Token response payload from the identity provider.
|
|
144
|
+
"""
|
|
145
|
+
data: dict[str, Any] = {
|
|
146
|
+
"grant_type": "password",
|
|
147
|
+
"username": username,
|
|
148
|
+
"password": password,
|
|
149
|
+
}
|
|
150
|
+
if self._scope:
|
|
151
|
+
data["scope"] = self._scope
|
|
152
|
+
return self._post_token(data)
|
|
153
|
+
|
|
154
|
+
def request_client_credentials_token(self) -> dict[str, Any]:
|
|
155
|
+
"""Request an access token using the client credentials grant.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
Token response payload from the identity provider.
|
|
160
|
+
"""
|
|
161
|
+
data: dict[str, Any] = {"grant_type": "client_credentials"}
|
|
162
|
+
if self._admin_scope:
|
|
163
|
+
data["scope"] = self._admin_scope
|
|
164
|
+
return self._post_token(data, use_admin=True)
|
|
165
|
+
|
|
166
|
+
def get_userinfo(self, access_token: str) -> dict[str, Any]:
|
|
167
|
+
"""Fetch user profile information for the given access token.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
access_token
|
|
172
|
+
Bearer access token.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
Userinfo response payload.
|
|
177
|
+
|
|
178
|
+
Raises
|
|
179
|
+
------
|
|
180
|
+
exceptions.MissingConfigurationException
|
|
181
|
+
If the userinfo endpoint is not configured.
|
|
182
|
+
"""
|
|
183
|
+
if not self._userinfo_url:
|
|
184
|
+
raise exceptions.MissingConfigurationException(
|
|
185
|
+
"userinfo_url is not configured"
|
|
186
|
+
)
|
|
187
|
+
return self._request_json(
|
|
188
|
+
"GET",
|
|
189
|
+
self._userinfo_url,
|
|
190
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def introspect_token(self, token: str) -> dict[str, Any]:
|
|
194
|
+
"""Introspect a token using the configured endpoint.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
token
|
|
199
|
+
Access or refresh token to introspect.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
Introspection response payload.
|
|
204
|
+
|
|
205
|
+
Raises
|
|
206
|
+
------
|
|
207
|
+
exceptions.MissingConfigurationException
|
|
208
|
+
If the introspection endpoint is not configured.
|
|
209
|
+
"""
|
|
210
|
+
if not self._introspection_url:
|
|
211
|
+
raise exceptions.MissingConfigurationException(
|
|
212
|
+
"introspection_url is not configured"
|
|
213
|
+
)
|
|
214
|
+
data = {"token": token}
|
|
215
|
+
return self._request_json(
|
|
216
|
+
"POST",
|
|
217
|
+
self._introspection_url,
|
|
218
|
+
data=data,
|
|
219
|
+
auth=self._build_auth(),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def get_user_by_subject(self, subject: str) -> dict[str, Any]:
|
|
223
|
+
"""Look up a user by subject using the admin client.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
subject
|
|
228
|
+
Subject or username to look up.
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
User representation returned by the provider.
|
|
233
|
+
|
|
234
|
+
Raises
|
|
235
|
+
------
|
|
236
|
+
exceptions.MissingConfigurationException
|
|
237
|
+
If the user lookup URL template is not configured.
|
|
238
|
+
exceptions.IdentityError
|
|
239
|
+
If an admin token cannot be obtained or response is invalid.
|
|
240
|
+
exceptions.UserNotFoundException
|
|
241
|
+
If the user is not found.
|
|
242
|
+
"""
|
|
243
|
+
if not self._user_lookup_url_template:
|
|
244
|
+
raise exceptions.MissingConfigurationException(
|
|
245
|
+
"user_lookup_url_template is not configured"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
token_data = self.request_client_credentials_token()
|
|
249
|
+
access_token = token_data.get("access_token")
|
|
250
|
+
if not access_token:
|
|
251
|
+
raise exceptions.IdentityError(
|
|
252
|
+
"Unable to obtain admin access token"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# URL-encode the subject to prevent format string injection
|
|
256
|
+
url = self._user_lookup_url_template.format(subject=quote(subject, safe=''))
|
|
257
|
+
response: Any = self._request_json(
|
|
258
|
+
"GET",
|
|
259
|
+
url,
|
|
260
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if isinstance(response, list):
|
|
264
|
+
if not response:
|
|
265
|
+
raise exceptions.UserNotFoundException(
|
|
266
|
+
f"User '{subject}' not found by identity provider"
|
|
267
|
+
)
|
|
268
|
+
first_item = cast(Mapping[str, Any], response[0])
|
|
269
|
+
return dict(first_item)
|
|
270
|
+
|
|
271
|
+
if isinstance(response, Mapping):
|
|
272
|
+
return dict(cast(Mapping[str, Any], response))
|
|
273
|
+
|
|
274
|
+
raise exceptions.IdentityError(
|
|
275
|
+
"Unexpected response while fetching user"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def _post_token(
|
|
279
|
+
self, data: dict[str, Any], use_admin: bool = False
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
"""Request a token at the token endpoint.
|
|
282
|
+
|
|
283
|
+
Parameters
|
|
284
|
+
----------
|
|
285
|
+
data
|
|
286
|
+
Form payload to send to the token endpoint.
|
|
287
|
+
use_admin, optional
|
|
288
|
+
Whether to use admin client credentials, by default False.
|
|
289
|
+
|
|
290
|
+
Returns
|
|
291
|
+
-------
|
|
292
|
+
Token response payload from the identity provider.
|
|
293
|
+
|
|
294
|
+
Raises
|
|
295
|
+
------
|
|
296
|
+
exceptions.MissingConfigurationException
|
|
297
|
+
If the required client identifier is missing.
|
|
298
|
+
"""
|
|
299
|
+
if use_admin:
|
|
300
|
+
client_id = self._admin_client_id
|
|
301
|
+
client_secret = self._admin_client_secret
|
|
302
|
+
else:
|
|
303
|
+
client_id = self._client_id
|
|
304
|
+
client_secret = self._client_secret
|
|
305
|
+
|
|
306
|
+
if not client_id:
|
|
307
|
+
raise exceptions.MissingConfigurationException(
|
|
308
|
+
"client_id is not configured"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if not self._token_url:
|
|
312
|
+
raise exceptions.MissingConfigurationException(
|
|
313
|
+
"token_url is not configured"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if not self._use_basic_auth:
|
|
317
|
+
data["client_id"] = client_id
|
|
318
|
+
if client_secret:
|
|
319
|
+
data["client_secret"] = client_secret
|
|
320
|
+
|
|
321
|
+
auth = (
|
|
322
|
+
HTTPBasicAuth(client_id, client_secret)
|
|
323
|
+
if self._use_basic_auth and client_secret
|
|
324
|
+
else None
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return self._request_json(
|
|
328
|
+
method="POST",
|
|
329
|
+
url=self._token_url,
|
|
330
|
+
data=data,
|
|
331
|
+
auth=auth,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def _request_json(
|
|
335
|
+
self,
|
|
336
|
+
method: str,
|
|
337
|
+
url: str,
|
|
338
|
+
**kwargs: Any,
|
|
339
|
+
) -> Any:
|
|
340
|
+
"""Execute an HTTP request and return a JSON response.
|
|
341
|
+
|
|
342
|
+
Parameters
|
|
343
|
+
----------
|
|
344
|
+
method
|
|
345
|
+
HTTP method (GET, POST, etc.).
|
|
346
|
+
url
|
|
347
|
+
Absolute URL for the request.
|
|
348
|
+
**kwargs
|
|
349
|
+
Additional arguments passed to `requests.request()`.
|
|
350
|
+
|
|
351
|
+
Returns
|
|
352
|
+
-------
|
|
353
|
+
Parsed JSON response payload.
|
|
354
|
+
|
|
355
|
+
Raises
|
|
356
|
+
------
|
|
357
|
+
exceptions.IdentityError
|
|
358
|
+
If the request fails or returns a non-JSON response.
|
|
359
|
+
exceptions.InvalidCredentialsException
|
|
360
|
+
If the identity provider rejects credentials.
|
|
361
|
+
"""
|
|
362
|
+
try:
|
|
363
|
+
response = self._session.request(
|
|
364
|
+
method,
|
|
365
|
+
url,
|
|
366
|
+
timeout=self._timeout_seconds,
|
|
367
|
+
verify=self._verify_tls,
|
|
368
|
+
**kwargs,
|
|
369
|
+
)
|
|
370
|
+
except requests.RequestException as exc:
|
|
371
|
+
raise exceptions.IdentityError(
|
|
372
|
+
f"OAuth2 request failed: {exc}"
|
|
373
|
+
) from exc
|
|
374
|
+
|
|
375
|
+
if response.status_code >= 400:
|
|
376
|
+
message = self._extract_error_message(response)
|
|
377
|
+
if response.status_code in (400, 401, 403):
|
|
378
|
+
raise exceptions.InvalidCredentialsException(message)
|
|
379
|
+
raise exceptions.IdentityError(message)
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
return response.json()
|
|
383
|
+
except ValueError as exc:
|
|
384
|
+
raise exceptions.IdentityError(
|
|
385
|
+
"OAuth2 response did not include JSON payload"
|
|
386
|
+
) from exc
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
def _extract_error_message(response: requests.Response) -> str:
|
|
390
|
+
"""Extract a provider error message from an HTTP response.
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
response
|
|
395
|
+
HTTP response returned by the provider.
|
|
396
|
+
|
|
397
|
+
Returns
|
|
398
|
+
-------
|
|
399
|
+
Human-readable error message.
|
|
400
|
+
"""
|
|
401
|
+
try:
|
|
402
|
+
payload = response.json()
|
|
403
|
+
error = payload.get("error_description") or payload.get("error")
|
|
404
|
+
if error:
|
|
405
|
+
return str(error)
|
|
406
|
+
except ValueError:
|
|
407
|
+
# If the response body is not valid JSON, fall back to a generic message.
|
|
408
|
+
pass
|
|
409
|
+
return f"OAuth2 request failed with status {response.status_code}"
|
|
410
|
+
|
|
411
|
+
def _build_url(self, url: str | None) -> str | None:
|
|
412
|
+
"""Resolve a relative endpoint against the configured base URL.
|
|
413
|
+
|
|
414
|
+
Parameters
|
|
415
|
+
----------
|
|
416
|
+
url
|
|
417
|
+
Absolute or relative URL.
|
|
418
|
+
|
|
419
|
+
Returns
|
|
420
|
+
-------
|
|
421
|
+
Absolute URL when possible, otherwise None.
|
|
422
|
+
"""
|
|
423
|
+
if url is None:
|
|
424
|
+
return None
|
|
425
|
+
if self._base_url and not url.startswith("http"):
|
|
426
|
+
return urljoin(self._base_url.rstrip("/") + "/", url)
|
|
427
|
+
return url
|
|
428
|
+
|
|
429
|
+
def _build_auth(self) -> HTTPBasicAuth | None:
|
|
430
|
+
"""Create HTTP Basic Auth credentials for introspection requests.
|
|
431
|
+
|
|
432
|
+
Returns
|
|
433
|
+
-------
|
|
434
|
+
Basic auth instance when configured, otherwise None.
|
|
435
|
+
"""
|
|
436
|
+
if not self._client_id or not self._client_secret:
|
|
437
|
+
return None
|
|
438
|
+
if not self._use_basic_auth:
|
|
439
|
+
return None
|
|
440
|
+
return HTTPBasicAuth(self._client_id, self._client_secret)
|
|
441
|
+
|
|
442
|
+
def _sanitize_scope(self, scope: str | list[str] | None) -> str | None:
|
|
443
|
+
"""Sanitize the scope parameter to be a space-delimited string.
|
|
444
|
+
|
|
445
|
+
Parameters
|
|
446
|
+
----------
|
|
447
|
+
scope
|
|
448
|
+
Scope as a string or list of strings.
|
|
449
|
+
|
|
450
|
+
Returns
|
|
451
|
+
-------
|
|
452
|
+
Space-delimited scope string or None.
|
|
453
|
+
"""
|
|
454
|
+
if scope is None:
|
|
455
|
+
return None
|
|
456
|
+
if isinstance(scope, list):
|
|
457
|
+
return " ".join(scope)
|
|
458
|
+
return scope
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class KeyCloakOIDCConnector(OIDCConnector):
|
|
462
|
+
"""Keycloak-specific OIDC connector."""
|
|
463
|
+
|
|
464
|
+
def __init__(
|
|
465
|
+
self,
|
|
466
|
+
base_url: str,
|
|
467
|
+
realm: str,
|
|
468
|
+
client_id: str,
|
|
469
|
+
client_secret: str,
|
|
470
|
+
scope: str | list[str] | None = None,
|
|
471
|
+
admin_client_id: str | None = None,
|
|
472
|
+
admin_client_secret: str | None = None,
|
|
473
|
+
admin_scope: str | None = None,
|
|
474
|
+
verify_tls: bool = True,
|
|
475
|
+
timeout_seconds: int = 10,
|
|
476
|
+
) -> None:
|
|
477
|
+
token_url = (
|
|
478
|
+
f"{base_url}/realms/{realm}" "/protocol/openid-connect/token"
|
|
479
|
+
)
|
|
480
|
+
userinfo_url = (
|
|
481
|
+
f"{base_url}/realms/{realm}" "/protocol/openid-connect/userinfo"
|
|
482
|
+
)
|
|
483
|
+
introspection_url = (
|
|
484
|
+
f"{base_url}/realms/{realm}"
|
|
485
|
+
"/protocol/openid-connect/token/introspect"
|
|
486
|
+
)
|
|
487
|
+
user_lookup_url_template = (
|
|
488
|
+
f"{base_url}/admin/realms/{realm}/users" "?username={subject}"
|
|
489
|
+
)
|
|
490
|
+
super().__init__(
|
|
491
|
+
token_url=token_url,
|
|
492
|
+
userinfo_url=userinfo_url,
|
|
493
|
+
introspection_url=introspection_url,
|
|
494
|
+
client_id=client_id,
|
|
495
|
+
client_secret=client_secret,
|
|
496
|
+
scope=scope,
|
|
497
|
+
verify_tls=verify_tls,
|
|
498
|
+
timeout_seconds=timeout_seconds,
|
|
499
|
+
user_lookup_url_template=user_lookup_url_template,
|
|
500
|
+
admin_client_id=admin_client_id,
|
|
501
|
+
admin_client_secret=admin_client_secret,
|
|
502
|
+
admin_scope=admin_scope,
|
|
503
|
+
)
|
|
@@ -10,6 +10,7 @@ from alpha.providers.models.token import Token
|
|
|
10
10
|
|
|
11
11
|
# Providers
|
|
12
12
|
from alpha.providers.ldap_provider import LDAPProvider, ADProvider
|
|
13
|
+
from alpha.providers.oidc_provider import OIDCProvider, KeyCloakProvider
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
16
|
"Identity",
|
|
@@ -20,4 +21,6 @@ __all__ = [
|
|
|
20
21
|
"Token",
|
|
21
22
|
"LDAPProvider",
|
|
22
23
|
"ADProvider",
|
|
24
|
+
"OIDCProvider",
|
|
25
|
+
"KeyCloakProvider",
|
|
23
26
|
]
|