cmem-client 0.5.0__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.
- cmem_client/__init__.py +13 -0
- cmem_client/auth_provider/__init__.py +14 -0
- cmem_client/auth_provider/abc.py +124 -0
- cmem_client/auth_provider/client_credentials.py +207 -0
- cmem_client/auth_provider/password.py +252 -0
- cmem_client/auth_provider/prefetched_token.py +153 -0
- cmem_client/client.py +485 -0
- cmem_client/components/__init__.py +10 -0
- cmem_client/components/graph_store.py +316 -0
- cmem_client/components/marketplace.py +179 -0
- cmem_client/components/sparql_wrapper.py +53 -0
- cmem_client/components/workspace.py +194 -0
- cmem_client/config.py +364 -0
- cmem_client/exceptions.py +82 -0
- cmem_client/logging_utils.py +49 -0
- cmem_client/models/__init__.py +16 -0
- cmem_client/models/access_condition.py +147 -0
- cmem_client/models/base.py +30 -0
- cmem_client/models/dataset.py +32 -0
- cmem_client/models/error.py +67 -0
- cmem_client/models/graph.py +26 -0
- cmem_client/models/item.py +143 -0
- cmem_client/models/logging_config.py +51 -0
- cmem_client/models/package.py +35 -0
- cmem_client/models/project.py +46 -0
- cmem_client/models/python_package.py +26 -0
- cmem_client/models/token.py +40 -0
- cmem_client/models/url.py +34 -0
- cmem_client/models/workflow.py +80 -0
- cmem_client/repositories/__init__.py +15 -0
- cmem_client/repositories/access_conditions.py +62 -0
- cmem_client/repositories/base/__init__.py +12 -0
- cmem_client/repositories/base/abc.py +138 -0
- cmem_client/repositories/base/paged_list.py +63 -0
- cmem_client/repositories/base/plain_list.py +39 -0
- cmem_client/repositories/base/task_search.py +70 -0
- cmem_client/repositories/datasets.py +36 -0
- cmem_client/repositories/graph_imports.py +93 -0
- cmem_client/repositories/graphs.py +458 -0
- cmem_client/repositories/marketplace_packages.py +486 -0
- cmem_client/repositories/projects.py +214 -0
- cmem_client/repositories/protocols/__init__.py +15 -0
- cmem_client/repositories/protocols/create_item.py +125 -0
- cmem_client/repositories/protocols/delete_item.py +95 -0
- cmem_client/repositories/protocols/export_item.py +114 -0
- cmem_client/repositories/protocols/import_item.py +141 -0
- cmem_client/repositories/python_packages.py +58 -0
- cmem_client/repositories/workflows.py +143 -0
- cmem_client-0.5.0.dist-info/METADATA +64 -0
- cmem_client-0.5.0.dist-info/RECORD +52 -0
- cmem_client-0.5.0.dist-info/WHEEL +4 -0
- cmem_client-0.5.0.dist-info/licenses/LICENSE +201 -0
cmem_client/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Next generation eccenca Corporate Memory client library.
|
|
2
|
+
|
|
3
|
+
This package provides a modern Python API client for eccenca Corporate Memory,
|
|
4
|
+
featuring:
|
|
5
|
+
- Pydantic-based data validation and serialization
|
|
6
|
+
- Async HTTP operations with httpx
|
|
7
|
+
- Comprehensive type hints with mypy support
|
|
8
|
+
- Flexible authentication providers
|
|
9
|
+
- Repository pattern for organized data access
|
|
10
|
+
|
|
11
|
+
The main entry point is the Client class, which can be configured manually
|
|
12
|
+
or automatically from environment variables.
|
|
13
|
+
"""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Authentication providers for Corporate Memory client.
|
|
2
|
+
|
|
3
|
+
This package contains various authentication provider implementations for connecting
|
|
4
|
+
to eccenca Corporate Memory instances. It supports multiple OAuth 2.0 flows and
|
|
5
|
+
authentication methods to accommodate different deployment scenarios.
|
|
6
|
+
|
|
7
|
+
Supported authentication methods:
|
|
8
|
+
- Client Credentials Flow: For machine-to-machine authentication
|
|
9
|
+
- Resource Owner Password Flow: For trusted applications with user credentials
|
|
10
|
+
- Prefetched Token: For scenarios where tokens are obtained externally
|
|
11
|
+
|
|
12
|
+
The AuthProvider abstract base class provides a common interface, and specific
|
|
13
|
+
implementations handle the details of each authentication method.
|
|
14
|
+
"""
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Abstract base class and factory for authentication providers.
|
|
2
|
+
|
|
3
|
+
This module defines the AuthProvider abstract base class that establishes the
|
|
4
|
+
interface all authentication providers must implement. It also provides a
|
|
5
|
+
factory method that automatically selects the appropriate authentication
|
|
6
|
+
provider based on environment variables.
|
|
7
|
+
|
|
8
|
+
The factory method supports automatic configuration from environment variables,
|
|
9
|
+
making it easy to switch between different authentication methods without
|
|
10
|
+
code changes by simply setting the OAUTH_GRANT_TYPE environment variable.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from os import getenv
|
|
16
|
+
|
|
17
|
+
from cmem_client.config import Config
|
|
18
|
+
from cmem_client.exceptions import ClientEnvConfigError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthProvider(ABC):
|
|
22
|
+
"""Abstract base class for authentication providers.
|
|
23
|
+
|
|
24
|
+
AuthProvider defines the common interface that all authentication providers
|
|
25
|
+
must implement to work with the Corporate Memory client. It provides the
|
|
26
|
+
contract for obtaining access tokens and includes a factory method for
|
|
27
|
+
creating appropriate provider instances based on environment configuration.
|
|
28
|
+
|
|
29
|
+
All concrete authentication provider implementations must inherit from this
|
|
30
|
+
class and implement the get_access_token method. The class also provides
|
|
31
|
+
automatic provider selection through environment variables.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
logger: logging.Logger
|
|
35
|
+
"""The logger for the auth provider."""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_access_token(self) -> str:
|
|
39
|
+
"""Get the access token for Bearer Authorization header.
|
|
40
|
+
|
|
41
|
+
This method must be implemented by all concrete authentication providers
|
|
42
|
+
to return a valid access token that can be used in HTTP Authorization
|
|
43
|
+
headers for API requests.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A valid access token string.
|
|
47
|
+
|
|
48
|
+
Note:
|
|
49
|
+
Implementations should handle token refresh logic internally when
|
|
50
|
+
tokens expire, ensuring this method always returns a valid token.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_env(cls, config: Config) -> "AuthProvider":
|
|
55
|
+
"""Create an authentication provider from environment variables.
|
|
56
|
+
|
|
57
|
+
This factory method automatically selects and configures the appropriate
|
|
58
|
+
authentication provider based on the OAUTH_GRANT_TYPE environment variable.
|
|
59
|
+
It supports multiple OAuth 2.0 flows and authentication methods.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
config: Configuration object containing Corporate Memory connection
|
|
63
|
+
details and endpoint URLs.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A configured AuthProvider instance appropriate for the environment
|
|
67
|
+
configuration.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ClientEnvConfigError: If the OAUTH_GRANT_TYPE is not supported or
|
|
71
|
+
if required environment variables for the selected provider
|
|
72
|
+
are missing.
|
|
73
|
+
|
|
74
|
+
Environment Variables:
|
|
75
|
+
OAUTH_GRANT_TYPE (optional): The OAuth flow type. Defaults to
|
|
76
|
+
"client_credentials". Supported values:
|
|
77
|
+
- "client_credentials": Client Credentials Flow for M2M auth
|
|
78
|
+
- "password": Resource Owner Password Flow for trusted apps
|
|
79
|
+
- "prefetched_token": Use externally obtained access token
|
|
80
|
+
"""
|
|
81
|
+
oauth_grant_type = getenv("OAUTH_GRANT_TYPE", "client_credentials")
|
|
82
|
+
|
|
83
|
+
if oauth_grant_type == "prefetched_token":
|
|
84
|
+
from cmem_client.auth_provider.prefetched_token import PrefetchedToken # noqa: PLC0415
|
|
85
|
+
|
|
86
|
+
return PrefetchedToken.from_env(config=config)
|
|
87
|
+
|
|
88
|
+
if oauth_grant_type == "client_credentials":
|
|
89
|
+
from cmem_client.auth_provider.client_credentials import ClientCredentialsFlow # noqa: PLC0415
|
|
90
|
+
|
|
91
|
+
return ClientCredentialsFlow.from_env(config=config)
|
|
92
|
+
|
|
93
|
+
if oauth_grant_type == "password":
|
|
94
|
+
from cmem_client.auth_provider.password import PasswordFlow # noqa: PLC0415
|
|
95
|
+
|
|
96
|
+
return PasswordFlow.from_env(config=config)
|
|
97
|
+
|
|
98
|
+
raise ClientEnvConfigError(f"No auth_provider configurable for the current environment ({oauth_grant_type})")
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_cmempy(cls, config: Config) -> "AuthProvider":
|
|
102
|
+
"""Create an authentication provider from a cmempy environment."""
|
|
103
|
+
try:
|
|
104
|
+
import cmem.cmempy.config as cmempy_config # noqa: PLC0415
|
|
105
|
+
except ImportError as error:
|
|
106
|
+
raise OSError("cmempy is not installed.") from error
|
|
107
|
+
oauth_grant_type = cmempy_config.get_oauth_grant_type()
|
|
108
|
+
|
|
109
|
+
if oauth_grant_type == "prefetched_token":
|
|
110
|
+
from cmem_client.auth_provider.prefetched_token import PrefetchedToken # noqa: PLC0415
|
|
111
|
+
|
|
112
|
+
return PrefetchedToken.from_cmempy(config=config)
|
|
113
|
+
|
|
114
|
+
if oauth_grant_type == "client_credentials":
|
|
115
|
+
from cmem_client.auth_provider.client_credentials import ClientCredentialsFlow # noqa: PLC0415
|
|
116
|
+
|
|
117
|
+
return ClientCredentialsFlow.from_cmempy(config=config)
|
|
118
|
+
|
|
119
|
+
if oauth_grant_type == "password":
|
|
120
|
+
from cmem_client.auth_provider.password import PasswordFlow # noqa: PLC0415
|
|
121
|
+
|
|
122
|
+
return PasswordFlow.from_cmempy(config=config)
|
|
123
|
+
|
|
124
|
+
raise ClientEnvConfigError(f"No auth_provider configurable for the current environment ({oauth_grant_type})")
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Client Credentials OAuth 2.0 flow authentication provider.
|
|
2
|
+
|
|
3
|
+
This module implements the Client Credentials Flow authentication method for
|
|
4
|
+
accessing eccenca Corporate Memory via OAuth 2.0. This flow is designed for
|
|
5
|
+
machine-to-machine authentication where no user interaction is required.
|
|
6
|
+
|
|
7
|
+
The Client Credentials Flow exchanges client ID and client secret for an access
|
|
8
|
+
token directly with the authorization server. It's ideal for backend services,
|
|
9
|
+
APIs, and automated systems that need to authenticate without user involvement.
|
|
10
|
+
|
|
11
|
+
This implementation handles token caching and automatic renewal when tokens expire.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from os import getenv
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from cmem_client.auth_provider.abc import AuthProvider
|
|
20
|
+
from cmem_client.config import Config
|
|
21
|
+
from cmem_client.exceptions import ClientEnvConfigError
|
|
22
|
+
from cmem_client.models.token import KeycloakToken
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClientCredentialsFlow(AuthProvider):
|
|
26
|
+
"""Client Credentials OAuth 2.0 flow authentication provider.
|
|
27
|
+
|
|
28
|
+
Implements the Client Credentials Flow (RFC 6749, section 4.4) for machine-to-machine
|
|
29
|
+
authentication with Corporate Memory via Keycloak. This flow exchanges client credentials
|
|
30
|
+
(client ID and secret) directly for access tokens without user interaction.
|
|
31
|
+
|
|
32
|
+
The provider handles automatic token caching and refresh, ensuring that get_access_token()
|
|
33
|
+
always returns a valid, non-expired token. It's designed for backend services, CLIs,
|
|
34
|
+
daemons, and other automated systems that need to authenticate as an application
|
|
35
|
+
rather than on behalf of a user.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
client_id: The OAuth 2.0 client identifier for the application.
|
|
39
|
+
client_secret: The confidential client secret for authentication.
|
|
40
|
+
config: Corporate Memory configuration containing endpoint URLs.
|
|
41
|
+
httpx: HTTP client for making token requests to the OAuth server.
|
|
42
|
+
token: Currently cached Keycloak token with expiration tracking.
|
|
43
|
+
|
|
44
|
+
See Also:
|
|
45
|
+
https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow
|
|
46
|
+
https://tools.ietf.org/html/rfc6749#section-4.4
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
client_id: str
|
|
50
|
+
"""OAuth 2.0 client identifier used to identify the application to the authorization server."""
|
|
51
|
+
|
|
52
|
+
client_secret: str
|
|
53
|
+
"""Confidential client secret used to authenticate the application with the OAuth server."""
|
|
54
|
+
|
|
55
|
+
config: Config
|
|
56
|
+
"""Corporate Memory configuration containing OAuth token endpoint and other URLs."""
|
|
57
|
+
|
|
58
|
+
httpx: httpx.Client
|
|
59
|
+
"""HTTP client instance used for making requests to the OAuth token endpoint."""
|
|
60
|
+
|
|
61
|
+
token: KeycloakToken
|
|
62
|
+
"""Currently cached access token with automatic expiration tracking and JWT parsing."""
|
|
63
|
+
|
|
64
|
+
logger: logging.Logger
|
|
65
|
+
"""Logger object for logging."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, config: Config, client_id: str, client_secret: str) -> None:
|
|
68
|
+
"""Initialize a new Client Credentials Flow authentication provider.
|
|
69
|
+
|
|
70
|
+
Creates a new provider instance and immediately fetches an initial access
|
|
71
|
+
token. The provider will handle token refresh automatically when needed.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
config: Corporate Memory configuration containing OAuth endpoint URLs
|
|
75
|
+
and other connection details.
|
|
76
|
+
client_id: The OAuth 2.0 client identifier registered with the
|
|
77
|
+
authorization server.
|
|
78
|
+
client_secret: The confidential client secret associated with the
|
|
79
|
+
client_id for authentication.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
HTTPError: If the initial token request fails due to network issues
|
|
83
|
+
or invalid credentials.
|
|
84
|
+
ValidationError: If the token response cannot be parsed as a valid
|
|
85
|
+
Keycloak token.
|
|
86
|
+
|
|
87
|
+
Note:
|
|
88
|
+
The constructor makes an immediate HTTP request to fetch the initial
|
|
89
|
+
token, so ensure network connectivity and valid credentials before
|
|
90
|
+
instantiation.
|
|
91
|
+
"""
|
|
92
|
+
self.config = config
|
|
93
|
+
self.client_id = client_id
|
|
94
|
+
self.client_secret = client_secret
|
|
95
|
+
self.httpx = httpx.Client()
|
|
96
|
+
self.logger = logging.getLogger(__name__)
|
|
97
|
+
self.token = self.fetch_new_token()
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_env(cls, config: Config) -> "ClientCredentialsFlow":
|
|
101
|
+
"""Create a Client Credentials Flow provider from environment variables.
|
|
102
|
+
|
|
103
|
+
This factory method creates a provider instance by reading OAuth client
|
|
104
|
+
credentials from environment variables. It's the recommended way to
|
|
105
|
+
create providers in production environments where credentials are
|
|
106
|
+
managed externally.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
config: Corporate Memory configuration containing OAuth endpoint URLs.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
A configured ClientCredentialsFlow instance ready for use.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ClientEnvConfigError: If the required OAUTH_CLIENT_SECRET environment
|
|
116
|
+
variable is not set.
|
|
117
|
+
|
|
118
|
+
Environment Variables:
|
|
119
|
+
OAUTH_CLIENT_ID (optional): The OAuth 2.0 client identifier.
|
|
120
|
+
Defaults to "cmem-service-account" if not specified.
|
|
121
|
+
OAUTH_CLIENT_SECRET (required): The confidential client secret
|
|
122
|
+
for authentication. Must be provided.
|
|
123
|
+
|
|
124
|
+
Security Note:
|
|
125
|
+
Client secrets should be stored securely and never committed to
|
|
126
|
+
version control. Use environment variables or secure secret
|
|
127
|
+
management systems in production.
|
|
128
|
+
"""
|
|
129
|
+
oauth_client_id = getenv("OAUTH_CLIENT_ID", "cmem-service-account")
|
|
130
|
+
oauth_client_secret = getenv("OAUTH_CLIENT_SECRET")
|
|
131
|
+
if not oauth_client_secret:
|
|
132
|
+
raise ClientEnvConfigError("Need OAUTH_CLIENT_SECRET environment variable.")
|
|
133
|
+
return cls(client_secret=oauth_client_secret, client_id=oauth_client_id, config=config)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_cmempy(cls, config: Config) -> "ClientCredentialsFlow":
|
|
137
|
+
"""Create a Client Credentials Flow provider from a cmempy environment."""
|
|
138
|
+
try:
|
|
139
|
+
import cmem.cmempy.config as cmempy_config # noqa: PLC0415
|
|
140
|
+
except ImportError as error:
|
|
141
|
+
raise OSError("cmempy is not installed.") from error
|
|
142
|
+
oauth_client_id = cmempy_config.get_oauth_client_id()
|
|
143
|
+
oauth_client_secret = cmempy_config.get_oauth_client_secret()
|
|
144
|
+
if not oauth_client_secret:
|
|
145
|
+
raise ClientEnvConfigError("Need OAUTH_CLIENT_SECRET environment variable.")
|
|
146
|
+
return cls(client_secret=oauth_client_secret, client_id=oauth_client_id, config=config)
|
|
147
|
+
|
|
148
|
+
def get_access_token(self) -> str:
|
|
149
|
+
"""Get a valid access token for Bearer Authorization header.
|
|
150
|
+
|
|
151
|
+
Returns a valid access token, automatically handling token refresh when
|
|
152
|
+
the current token is expired or near expiration. This method implements
|
|
153
|
+
intelligent caching to minimize unnecessary token requests while ensuring
|
|
154
|
+
the returned token is always valid.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
A valid access token string ready for use in HTTP Authorization headers.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
HTTPError: If token refresh fails due to network issues or invalid
|
|
161
|
+
credentials.
|
|
162
|
+
ValidationError: If the token response cannot be parsed.
|
|
163
|
+
"""
|
|
164
|
+
if self.token.is_expired():
|
|
165
|
+
self.logger.debug("Access token expired, refreshing...")
|
|
166
|
+
self.token = self.fetch_new_token()
|
|
167
|
+
return self.token.access_token
|
|
168
|
+
|
|
169
|
+
def fetch_new_token(self) -> KeycloakToken:
|
|
170
|
+
"""Fetch a new access token from the OAuth 2.0 token endpoint.
|
|
171
|
+
|
|
172
|
+
Makes an HTTP POST request to the Keycloak token endpoint using the
|
|
173
|
+
Client Credentials Flow parameters. The response is parsed and returned
|
|
174
|
+
as a KeycloakToken object with automatic expiration tracking.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A new KeycloakToken instance with the fresh access token and
|
|
178
|
+
expiration information.
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
HTTPError: If the token request fails due to network issues,
|
|
182
|
+
invalid credentials, or server errors.
|
|
183
|
+
ValidationError: If the token response cannot be parsed as a
|
|
184
|
+
valid Keycloak token format.
|
|
185
|
+
|
|
186
|
+
Note:
|
|
187
|
+
This method performs a synchronous HTTP request and should not be
|
|
188
|
+
called directly in most cases. Use get_access_token() instead,
|
|
189
|
+
which handles caching and only calls this method when necessary.
|
|
190
|
+
|
|
191
|
+
Implementation Details:
|
|
192
|
+
- Uses the standard OAuth 2.0 Client Credentials Flow parameters
|
|
193
|
+
- Sends credentials in the request body (not in Authorization header)
|
|
194
|
+
- Automatically decodes the JSON response and validates the format
|
|
195
|
+
- Extracts JWT claims for expiration tracking
|
|
196
|
+
"""
|
|
197
|
+
self.logger.debug("Fetching new access token from OAuth 2.0 token endpoint.")
|
|
198
|
+
post_data = {
|
|
199
|
+
"grant_type": "client_credentials",
|
|
200
|
+
"client_id": self.client_id,
|
|
201
|
+
"client_secret": self.client_secret,
|
|
202
|
+
}
|
|
203
|
+
response = self.httpx.post(url=self.config.url_oauth_token, data=post_data)
|
|
204
|
+
response.raise_for_status()
|
|
205
|
+
content = response.content.decode("utf-8")
|
|
206
|
+
self.logger.debug("Successfully fetched new access token from OAuth 2.0 token endpoint.")
|
|
207
|
+
return KeycloakToken.model_validate_json(json_data=content)
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Resource Owner Password OAuth 2.0 flow authentication provider.
|
|
2
|
+
|
|
3
|
+
This module implements the Resource Owner Password Flow authentication method,
|
|
4
|
+
which allows highly-trusted applications to authenticate users by collecting
|
|
5
|
+
their username and password credentials directly.
|
|
6
|
+
|
|
7
|
+
Security Warning: This flow should only be used by absolutely trusted
|
|
8
|
+
applications as it requires handling user passwords directly. It's typically
|
|
9
|
+
used for legacy applications or first-party applications where other OAuth flows
|
|
10
|
+
are not feasible.
|
|
11
|
+
|
|
12
|
+
This implementation handles token caching and automatic renewal when tokens expire,
|
|
13
|
+
similar to the Client Credentials Flow but using username/password credentials.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from os import getenv
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
from cmem_client.auth_provider.abc import AuthProvider
|
|
22
|
+
from cmem_client.config import Config
|
|
23
|
+
from cmem_client.exceptions import ClientEnvConfigError
|
|
24
|
+
from cmem_client.models.token import KeycloakToken
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PasswordFlow(AuthProvider):
|
|
28
|
+
"""Resource Owner Password OAuth 2.0 flow authentication provider.
|
|
29
|
+
|
|
30
|
+
Security Warning: This authentication flow should only be used by
|
|
31
|
+
absolutely trusted applications as it requires handling user passwords directly.
|
|
32
|
+
|
|
33
|
+
Implements the Resource Owner Password Flow (RFC 6749, section 4.3) for
|
|
34
|
+
authentication with Corporate Memory via Keycloak. This flow exchanges user
|
|
35
|
+
credentials (username and password) directly for access tokens, bypassing
|
|
36
|
+
the standard OAuth 2.0 authorization code flow.
|
|
37
|
+
|
|
38
|
+
This provider handles automatic token caching and refresh, ensuring that
|
|
39
|
+
get_access_token() always returns a valid token. It's typically used for
|
|
40
|
+
legacy applications, first-party applications, or scenarios where the standard
|
|
41
|
+
OAuth flows are not feasible.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
client_id: The OAuth 2.0 client identifier for the application.
|
|
45
|
+
username: The user's username for authentication.
|
|
46
|
+
password: The user's password for authentication.
|
|
47
|
+
config: Corporate Memory configuration containing endpoint URLs.
|
|
48
|
+
httpx: HTTP client for making token requests to the OAuth server.
|
|
49
|
+
token: Currently cached Keycloak token with expiration tracking.
|
|
50
|
+
|
|
51
|
+
Security Considerations:
|
|
52
|
+
- User credentials are sent directly to the authorization server
|
|
53
|
+
- Passwords may be stored in memory for token refresh purposes
|
|
54
|
+
- Only use in highly trusted applications with secure credential handling
|
|
55
|
+
- Consider using Client Credentials Flow for machine-to-machine auth instead
|
|
56
|
+
|
|
57
|
+
See Also:
|
|
58
|
+
https://auth0.com/docs/get-started/authentication-and-authorization-flow/resource-owner-password-flow
|
|
59
|
+
https://tools.ietf.org/html/rfc6749#section-4.3
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
client_id: str
|
|
63
|
+
"""OAuth 2.0 client identifier used to identify the application to the authorization server."""
|
|
64
|
+
|
|
65
|
+
username: str
|
|
66
|
+
"""User's username/email for authentication with the OAuth server."""
|
|
67
|
+
|
|
68
|
+
password: str
|
|
69
|
+
"""User's password for authentication. ⚠️ Stored in memory for token refresh."""
|
|
70
|
+
|
|
71
|
+
config: Config
|
|
72
|
+
"""Corporate Memory configuration containing OAuth token endpoint and other URLs."""
|
|
73
|
+
|
|
74
|
+
httpx: httpx.Client
|
|
75
|
+
"""HTTP client instance used for making requests to the OAuth token endpoint."""
|
|
76
|
+
|
|
77
|
+
token: KeycloakToken
|
|
78
|
+
"""Currently cached access token with automatic expiration tracking and JWT parsing."""
|
|
79
|
+
|
|
80
|
+
logger: logging.Logger
|
|
81
|
+
"""Logger object used to log messages."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, config: Config, client_id: str, username: str, password: str) -> None:
|
|
84
|
+
"""Initialize a new Resource Owner Password Flow authentication provider.
|
|
85
|
+
|
|
86
|
+
Security Warning: This constructor stores the user's password in memory
|
|
87
|
+
for potential token refresh operations. Only use in absolutely trusted applications.
|
|
88
|
+
|
|
89
|
+
Creates a new provider instance and immediately fetches an initial access
|
|
90
|
+
token. The provider will handle token refresh automatically when needed.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
config: Corporate Memory configuration containing OAuth endpoint URLs
|
|
94
|
+
and other connection details.
|
|
95
|
+
client_id: The OAuth 2.0 client identifier registered with the
|
|
96
|
+
authorization server.
|
|
97
|
+
username: The user's username or email address for authentication.
|
|
98
|
+
password: The user's password for authentication.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
HTTPError: If the initial token request fails due to network issues
|
|
102
|
+
or invalid credentials.
|
|
103
|
+
ValidationError: If the token response cannot be parsed as a valid
|
|
104
|
+
Keycloak token.
|
|
105
|
+
|
|
106
|
+
Security Note:
|
|
107
|
+
The constructor makes an immediate HTTP request to fetch the initial
|
|
108
|
+
token, sending the user's credentials over the network. Ensure secure
|
|
109
|
+
network connections (HTTPS) and proper credential handling.
|
|
110
|
+
"""
|
|
111
|
+
self.config = config
|
|
112
|
+
self.client_id = client_id
|
|
113
|
+
self.username = username
|
|
114
|
+
self.password = password
|
|
115
|
+
self.httpx = httpx.Client()
|
|
116
|
+
self.logger = logging.getLogger(__name__)
|
|
117
|
+
self.token = self.fetch_new_token()
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_env(cls, config: Config) -> "PasswordFlow":
|
|
121
|
+
"""Create a Password Flow provider from environment variables.
|
|
122
|
+
|
|
123
|
+
Security Warning: This method reads user credentials from environment
|
|
124
|
+
variables, which may be visible in process lists or logs. Use with extreme caution.
|
|
125
|
+
|
|
126
|
+
This factory method creates a provider instance by reading user credentials
|
|
127
|
+
from environment variables. While more secure than hardcoded credentials,
|
|
128
|
+
environment variables should be properly protected in production environments.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
config: Corporate Memory configuration containing OAuth endpoint URLs.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
A configured PasswordFlow instance ready for use.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ClientEnvConfigError: If the required OAUTH_USER or OAUTH_PASSWORD
|
|
138
|
+
environment variables are not set.
|
|
139
|
+
|
|
140
|
+
Environment Variables:
|
|
141
|
+
OAUTH_USER (required): The username or email address for authentication.
|
|
142
|
+
Must be a valid user account in the Corporate Memory system.
|
|
143
|
+
OAUTH_PASSWORD (required): The user's password for authentication.
|
|
144
|
+
Should be handled securely and not logged.
|
|
145
|
+
OAUTH_CLIENT_ID (optional): The OAuth 2.0 client identifier.
|
|
146
|
+
Defaults to "cmem-service-account" if not specified.
|
|
147
|
+
|
|
148
|
+
Security Notes:
|
|
149
|
+
- Environment variables may be visible in process lists
|
|
150
|
+
- Use secure credential management in production environments
|
|
151
|
+
- Consider using Client Credentials Flow for service accounts instead
|
|
152
|
+
- Ensure proper access controls on systems storing these credentials
|
|
153
|
+
"""
|
|
154
|
+
oauth_user = getenv("OAUTH_USER")
|
|
155
|
+
oauth_password = getenv("OAUTH_PASSWORD")
|
|
156
|
+
oauth_client_id = getenv("OAUTH_CLIENT_ID", "cmem-service-account")
|
|
157
|
+
if not oauth_user:
|
|
158
|
+
raise ClientEnvConfigError("Need OAUTH_USER environment variable.")
|
|
159
|
+
if not oauth_password:
|
|
160
|
+
raise ClientEnvConfigError("Need OAUTH_PASSWORD environment variable.")
|
|
161
|
+
return cls(username=oauth_user, password=oauth_password, config=config, client_id=oauth_client_id)
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def from_cmempy(cls, config: Config) -> "PasswordFlow":
|
|
165
|
+
"""Create a Password Flow provider from a cmempy environment."""
|
|
166
|
+
try:
|
|
167
|
+
import cmem.cmempy.config as cmempy_config # noqa: PLC0415
|
|
168
|
+
except ImportError as error:
|
|
169
|
+
raise OSError("cmempy is not installed.") from error
|
|
170
|
+
oauth_user = cmempy_config.get_oauth_user()
|
|
171
|
+
oauth_password = cmempy_config.get_oauth_password()
|
|
172
|
+
oauth_client_id = cmempy_config.get_oauth_client_id()
|
|
173
|
+
if not oauth_user:
|
|
174
|
+
raise ClientEnvConfigError("Need OAUTH_USER environment variable.")
|
|
175
|
+
if not oauth_password:
|
|
176
|
+
raise ClientEnvConfigError("Need OAUTH_PASSWORD environment variable.")
|
|
177
|
+
return cls(username=oauth_user, password=oauth_password, config=config, client_id=oauth_client_id)
|
|
178
|
+
|
|
179
|
+
def get_access_token(self) -> str:
|
|
180
|
+
"""Get a valid access token for Bearer Authorization header.
|
|
181
|
+
|
|
182
|
+
Returns a valid access token, automatically handling token refresh when
|
|
183
|
+
the current token is expired or near expiration. This method implements
|
|
184
|
+
intelligent caching to minimize unnecessary token requests while ensuring
|
|
185
|
+
the returned token is always valid.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
A valid access token string ready for use in HTTP Authorization headers.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
HTTPError: If token refresh fails due to network issues or invalid
|
|
192
|
+
credentials (username/password may have changed).
|
|
193
|
+
ValidationError: If the token response cannot be parsed.
|
|
194
|
+
|
|
195
|
+
Security Note:
|
|
196
|
+
Each token refresh operation sends the username and password over
|
|
197
|
+
the network. Ensure secure connections (HTTPS) and consider token
|
|
198
|
+
lifetime implications for security.
|
|
199
|
+
"""
|
|
200
|
+
if self.token.is_expired():
|
|
201
|
+
self.logger.debug("Access token expired, refreshing...")
|
|
202
|
+
self.token = self.fetch_new_token()
|
|
203
|
+
return self.token.access_token
|
|
204
|
+
|
|
205
|
+
def fetch_new_token(self) -> KeycloakToken:
|
|
206
|
+
"""Fetch a new access token from the OAuth 2.0 token endpoint.
|
|
207
|
+
|
|
208
|
+
Security Warning: This method sends user credentials (username and password)
|
|
209
|
+
over the network to the authorization server. Ensure secure connections (HTTPS).
|
|
210
|
+
|
|
211
|
+
Makes an HTTP POST request to the Keycloak token endpoint using the
|
|
212
|
+
Resource Owner Password Flow parameters. The response is parsed and returned
|
|
213
|
+
as a KeycloakToken object with automatic expiration tracking.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
A new KeycloakToken instance with the fresh access token and
|
|
217
|
+
expiration information.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
HTTPError: If the token request fails due to network issues,
|
|
221
|
+
invalid credentials, or server errors.
|
|
222
|
+
ValidationError: If the token response cannot be parsed as a
|
|
223
|
+
valid Keycloak token format.
|
|
224
|
+
|
|
225
|
+
Security Considerations:
|
|
226
|
+
- User credentials are sent in plaintext (over HTTPS)
|
|
227
|
+
- Consider the security implications of credential reuse for token refresh
|
|
228
|
+
- Monitor for credential compromise if tokens are frequently refreshed
|
|
229
|
+
|
|
230
|
+
Note:
|
|
231
|
+
This method performs a synchronous HTTP request and should not be
|
|
232
|
+
called directly in most cases. Use get_access_token() instead,
|
|
233
|
+
which handles caching and only calls this method when necessary.
|
|
234
|
+
|
|
235
|
+
Implementation Details:
|
|
236
|
+
- Uses the standard OAuth 2.0 Resource Owner Password Flow parameters
|
|
237
|
+
- Sends credentials in the request body (not in Authorization header)
|
|
238
|
+
- Automatically decodes the JSON response and validates the format
|
|
239
|
+
- Extracts JWT claims for expiration tracking
|
|
240
|
+
"""
|
|
241
|
+
self.logger.debug("Fetching new access token from OAuth 2.0 token endpoint.")
|
|
242
|
+
post_data = {
|
|
243
|
+
"grant_type": "password",
|
|
244
|
+
"client_id": self.client_id,
|
|
245
|
+
"username": self.username,
|
|
246
|
+
"password": self.password,
|
|
247
|
+
}
|
|
248
|
+
response = self.httpx.post(url=self.config.url_oauth_token, data=post_data)
|
|
249
|
+
response.raise_for_status()
|
|
250
|
+
content = response.content.decode("utf-8")
|
|
251
|
+
self.logger.debug("Successfully fetched new access token from OAuth 2.0 token endpoint.")
|
|
252
|
+
return KeycloakToken.model_validate_json(json_data=content)
|