python-alfresco-api 1.0.2__py3-none-any.whl → 1.1.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.
- python_alfresco_api/__init__.py +23 -10
- python_alfresco_api/auth_util.py +550 -19
- python_alfresco_api/client_factory.py +158 -39
- python_alfresco_api/clients/__init__.py +60 -1
- python_alfresco_api/clients/auth/__init__.py +14 -0
- python_alfresco_api/clients/auth/auth_client.py +167 -0
- python_alfresco_api/clients/auth/authentication/__init__.py +10 -0
- python_alfresco_api/clients/auth/authentication/authentication_client.py +94 -0
- python_alfresco_api/clients/auth/authentication/models.py +80 -0
- python_alfresco_api/clients/auth/models.py +55 -0
- python_alfresco_api/clients/conversion_utils.py +197 -0
- python_alfresco_api/clients/core/__init__.py +11 -0
- python_alfresco_api/clients/core/actions/__init__.py +10 -0
- python_alfresco_api/clients/core/actions/actions_client.py +320 -0
- python_alfresco_api/clients/core/actions/models.py +84 -0
- python_alfresco_api/clients/core/activities/__init__.py +10 -0
- python_alfresco_api/clients/core/activities/activities_client.py +141 -0
- python_alfresco_api/clients/core/activities/models.py +84 -0
- python_alfresco_api/clients/core/audit/__init__.py +10 -0
- python_alfresco_api/clients/core/audit/audit_client.py +95 -0
- python_alfresco_api/clients/core/audit/models.py +84 -0
- python_alfresco_api/clients/core/comments/__init__.py +10 -0
- python_alfresco_api/clients/core/comments/comments_client.py +320 -0
- python_alfresco_api/clients/core/comments/models.py +84 -0
- python_alfresco_api/clients/core/content/__init__.py +15 -0
- python_alfresco_api/clients/core/content/content_client.py +471 -0
- python_alfresco_api/clients/core/content/models.py +139 -0
- python_alfresco_api/clients/core/core_client.py +440 -0
- python_alfresco_api/clients/core/favorites/__init__.py +10 -0
- python_alfresco_api/clients/core/favorites/favorites_client.py +87 -0
- python_alfresco_api/clients/core/favorites/models.py +84 -0
- python_alfresco_api/clients/core/groups/__init__.py +10 -0
- python_alfresco_api/clients/core/groups/groups_client.py +87 -0
- python_alfresco_api/clients/core/groups/models.py +84 -0
- python_alfresco_api/clients/core/models.py +133 -0
- python_alfresco_api/clients/core/networks/__init__.py +10 -0
- python_alfresco_api/clients/core/networks/models.py +84 -0
- python_alfresco_api/clients/core/networks/networks_client.py +77 -0
- python_alfresco_api/clients/core/nodes/__init__.py +34 -0
- python_alfresco_api/clients/core/nodes/copy_node.py +282 -0
- python_alfresco_api/clients/core/nodes/create_association.py +222 -0
- python_alfresco_api/clients/core/nodes/create_folder.py +336 -0
- python_alfresco_api/clients/core/nodes/create_node.py +379 -0
- python_alfresco_api/clients/core/nodes/create_secondary_child_association.py +182 -0
- python_alfresco_api/clients/core/nodes/delete_association.py +145 -0
- python_alfresco_api/clients/core/nodes/delete_node.py +135 -0
- python_alfresco_api/clients/core/nodes/delete_secondary_child_association.py +145 -0
- python_alfresco_api/clients/core/nodes/get_node.py +246 -0
- python_alfresco_api/clients/core/nodes/list_node_children.py +304 -0
- python_alfresco_api/clients/core/nodes/list_parents.py +227 -0
- python_alfresco_api/clients/core/nodes/list_secondary_children.py +253 -0
- python_alfresco_api/clients/core/nodes/list_source_associations.py +283 -0
- python_alfresco_api/clients/core/nodes/list_target_associations.py +225 -0
- python_alfresco_api/clients/core/nodes/lock_node.py +237 -0
- python_alfresco_api/clients/core/nodes/models.py +265 -0
- python_alfresco_api/clients/core/nodes/move_node.py +282 -0
- python_alfresco_api/clients/core/nodes/nodes_client.py +436 -0
- python_alfresco_api/clients/core/nodes/unlock_node.py +197 -0
- python_alfresco_api/clients/core/nodes/update_node.py +297 -0
- python_alfresco_api/clients/core/nodes/update_node_content.py +276 -0
- python_alfresco_api/clients/core/people/__init__.py +10 -0
- python_alfresco_api/clients/core/people/models.py +84 -0
- python_alfresco_api/clients/core/people/people_client.py +83 -0
- python_alfresco_api/clients/core/preferences/__init__.py +10 -0
- python_alfresco_api/clients/core/preferences/models.py +84 -0
- python_alfresco_api/clients/core/preferences/preferences_client.py +75 -0
- python_alfresco_api/clients/core/probes/__init__.py +10 -0
- python_alfresco_api/clients/core/probes/models.py +84 -0
- python_alfresco_api/clients/core/probes/probes_client.py +102 -0
- python_alfresco_api/clients/core/queries/__init__.py +10 -0
- python_alfresco_api/clients/core/queries/models.py +84 -0
- python_alfresco_api/clients/core/queries/queries_client.py +278 -0
- python_alfresco_api/clients/core/ratings/__init__.py +10 -0
- python_alfresco_api/clients/core/ratings/models.py +84 -0
- python_alfresco_api/clients/core/ratings/ratings_client.py +255 -0
- python_alfresco_api/clients/core/renditions/__init__.py +10 -0
- python_alfresco_api/clients/core/renditions/models.py +84 -0
- python_alfresco_api/clients/core/renditions/renditions_client.py +170 -0
- python_alfresco_api/clients/core/shared_links/__init__.py +10 -0
- python_alfresco_api/clients/core/shared_links/models.py +84 -0
- python_alfresco_api/clients/core/shared_links/shared_links_client.py +89 -0
- python_alfresco_api/clients/core/sites/__init__.py +10 -0
- python_alfresco_api/clients/core/sites/models.py +84 -0
- python_alfresco_api/clients/core/sites/sites_client.py +113 -0
- python_alfresco_api/clients/core/tags/__init__.py +10 -0
- python_alfresco_api/clients/core/tags/models.py +84 -0
- python_alfresco_api/clients/core/tags/tags_client.py +83 -0
- python_alfresco_api/clients/core/trashcan/__init__.py +10 -0
- python_alfresco_api/clients/core/trashcan/models.py +84 -0
- python_alfresco_api/clients/core/trashcan/trashcan_client.py +87 -0
- python_alfresco_api/clients/core/versions/__init__.py +10 -0
- python_alfresco_api/clients/core/versions/models.py +117 -0
- python_alfresco_api/clients/core/versions/versions_client.py +412 -0
- python_alfresco_api/clients/discovery/__init__.py +14 -0
- python_alfresco_api/clients/discovery/discovery/__init__.py +12 -0
- python_alfresco_api/clients/discovery/discovery/discovery_operations.py +156 -0
- python_alfresco_api/clients/discovery/discovery/models.py +80 -0
- python_alfresco_api/clients/discovery/discovery_client.py +151 -0
- python_alfresco_api/clients/discovery/models.py +55 -0
- python_alfresco_api/clients/field_mapping.py +297 -0
- python_alfresco_api/clients/master_client.py +309 -0
- python_alfresco_api/clients/model/__init__.py +14 -0
- python_alfresco_api/clients/model/aspects/__init__.py +10 -0
- python_alfresco_api/clients/model/aspects/aspects_client.py +87 -0
- python_alfresco_api/clients/model/aspects/models.py +80 -0
- python_alfresco_api/clients/model/model_client.py +165 -0
- python_alfresco_api/clients/model/models.py +55 -0
- python_alfresco_api/clients/model/types/__init__.py +10 -0
- python_alfresco_api/clients/model/types/models.py +80 -0
- python_alfresco_api/clients/model/types/types_client.py +87 -0
- python_alfresco_api/clients/models.py +110 -0
- python_alfresco_api/clients/search/__init__.py +14 -0
- python_alfresco_api/clients/search/models.py +55 -0
- python_alfresco_api/clients/search/search/__init__.py +12 -0
- python_alfresco_api/clients/search/search/models.py +80 -0
- python_alfresco_api/clients/search/search/search_operations.py +157 -0
- python_alfresco_api/clients/search/search_client.py +151 -0
- python_alfresco_api/clients/search_sql/__init__.py +14 -0
- python_alfresco_api/clients/search_sql/models.py +55 -0
- python_alfresco_api/clients/search_sql/search_sql_client.py +150 -0
- python_alfresco_api/clients/search_sql/sql/__init__.py +10 -0
- python_alfresco_api/clients/search_sql/sql/models.py +80 -0
- python_alfresco_api/clients/search_sql/sql/sql_client.py +89 -0
- python_alfresco_api/clients/workflow/__init__.py +14 -0
- python_alfresco_api/clients/workflow/deployments/__init__.py +10 -0
- python_alfresco_api/clients/workflow/deployments/deployments_client.py +85 -0
- python_alfresco_api/clients/workflow/deployments/models.py +80 -0
- python_alfresco_api/clients/workflow/models.py +55 -0
- python_alfresco_api/clients/workflow/process_definitions/__init__.py +16 -0
- python_alfresco_api/clients/workflow/process_definitions/models.py +80 -0
- python_alfresco_api/clients/workflow/process_definitions/process_definitions_client.py +401 -0
- python_alfresco_api/clients/workflow/processes/__init__.py +16 -0
- python_alfresco_api/clients/workflow/processes/models.py +80 -0
- python_alfresco_api/clients/workflow/processes/processes_client.py +999 -0
- python_alfresco_api/clients/workflow/tasks/__init__.py +16 -0
- python_alfresco_api/clients/workflow/tasks/models.py +80 -0
- python_alfresco_api/clients/workflow/tasks/tasks_client.py +486 -0
- python_alfresco_api/clients/workflow/workflow_client.py +193 -0
- python_alfresco_api/models/alfresco_core_models.py +1288 -1288
- python_alfresco_api/raw_clients/alfresco_core_client/core_client/api/nodes/create_node.py +1327 -1327
- python_alfresco_api/raw_clients/alfresco_core_client/core_client/models/association.py +67 -67
- python_alfresco_api/raw_clients/alfresco_core_client/core_client/models/node.py +282 -282
- python_alfresco_api/raw_clients/alfresco_core_client/core_client/models/node_association.py +300 -300
- python_alfresco_api/utils/__init__.py +73 -0
- python_alfresco_api/utils/content_utils.py +289 -0
- python_alfresco_api/utils/content_utils_highlevel.py +393 -0
- python_alfresco_api/utils/mcp_formatters.py +426 -0
- python_alfresco_api/utils/node_utils.py +469 -0
- python_alfresco_api/utils/node_utils_highlevel.py +280 -0
- python_alfresco_api/utils/search_utils.py +445 -0
- python_alfresco_api/utils/version_utils.py +373 -0
- python_alfresco_api/utils/version_utils_highlevel.py +565 -0
- python_alfresco_api-1.1.0.dist-info/METADATA +652 -0
- {python_alfresco_api-1.0.2.dist-info → python_alfresco_api-1.1.0.dist-info}/RECORD +156 -24
- python_alfresco_api/clients/auth_client.py +0 -85
- python_alfresco_api/clients/core_client.py +0 -85
- python_alfresco_api/clients/discovery_client.py +0 -85
- python_alfresco_api/clients/model_client.py +0 -85
- python_alfresco_api/clients/search_client.py +0 -85
- python_alfresco_api/clients/search_sql_client.py +0 -85
- python_alfresco_api/clients/workflow_client.py +0 -85
- python_alfresco_api/examples/basic_usage.py +0 -52
- python_alfresco_api/examples/llm_integration.py +0 -155
- python_alfresco_api/tests/__init__.py +0 -1
- python_alfresco_api/tests/test_basic.py +0 -115
- python_alfresco_api-1.0.2.dist-info/METADATA +0 -796
- {python_alfresco_api-1.0.2.dist-info → python_alfresco_api-1.1.0.dist-info}/WHEEL +0 -0
- {python_alfresco_api-1.0.2.dist-info → python_alfresco_api-1.1.0.dist-info}/licenses/LICENSE +0 -0
python_alfresco_api/__init__.py
CHANGED
|
@@ -12,25 +12,33 @@ datamodel-code-generator + openapi-python-client
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from .client_factory import ClientFactory
|
|
15
|
-
from .auth_util import AuthUtil
|
|
15
|
+
from .auth_util import AuthUtil, OAuth2AuthUtil
|
|
16
16
|
|
|
17
|
-
# Individual clients
|
|
18
|
-
from .clients.
|
|
19
|
-
from .clients.
|
|
20
|
-
from .clients.
|
|
21
|
-
from .clients.
|
|
22
|
-
from .clients.
|
|
23
|
-
from .clients.
|
|
24
|
-
from .clients.
|
|
17
|
+
# Individual clients - V1.1 hierarchical structure
|
|
18
|
+
from .clients.auth import AlfrescoAuthClient
|
|
19
|
+
from .clients.core import AlfrescoCoreClient
|
|
20
|
+
from .clients.discovery import AlfrescoDiscoveryClient
|
|
21
|
+
from .clients.search import AlfrescoSearchClient
|
|
22
|
+
from .clients.workflow import AlfrescoWorkflowClient
|
|
23
|
+
from .clients.model import AlfrescoModelClient
|
|
24
|
+
from .clients.search_sql import AlfrescoSearchSqlClient
|
|
25
25
|
|
|
26
26
|
# Pydantic models for LLM integration
|
|
27
27
|
from .models import *
|
|
28
28
|
|
|
29
|
+
# Conversion utilities for Pydantic ↔ attrs model transformation
|
|
30
|
+
from .clients.conversion_utils import (
|
|
31
|
+
pydantic_to_attrs_dict,
|
|
32
|
+
attrs_to_pydantic,
|
|
33
|
+
create_converter_pair
|
|
34
|
+
)
|
|
35
|
+
|
|
29
36
|
__version__ = "1.0.0"
|
|
30
37
|
__all__ = [
|
|
31
38
|
# Factory & utilities
|
|
32
39
|
"ClientFactory",
|
|
33
40
|
"AuthUtil",
|
|
41
|
+
"OAuth2AuthUtil",
|
|
34
42
|
|
|
35
43
|
# Individual clients
|
|
36
44
|
"AlfrescoAuthClient",
|
|
@@ -39,5 +47,10 @@ __all__ = [
|
|
|
39
47
|
"AlfrescoSearchClient",
|
|
40
48
|
"AlfrescoWorkflowClient",
|
|
41
49
|
"AlfrescoModelClient",
|
|
42
|
-
"AlfrescoSearchSqlClient"
|
|
50
|
+
"AlfrescoSearchSqlClient",
|
|
51
|
+
|
|
52
|
+
# Conversion utilities
|
|
53
|
+
"pydantic_to_attrs_dict",
|
|
54
|
+
"attrs_to_pydantic",
|
|
55
|
+
"create_converter_pair"
|
|
43
56
|
]
|
python_alfresco_api/auth_util.py
CHANGED
|
@@ -6,8 +6,115 @@ Handles ticket-based authentication with automatic renewal.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
-
|
|
9
|
+
import base64
|
|
10
|
+
import os
|
|
11
|
+
from typing import Optional, Dict, Any, Union
|
|
10
12
|
from datetime import datetime, timedelta
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
# Try to import python-dotenv for .env file support (optional)
|
|
16
|
+
try:
|
|
17
|
+
from dotenv import load_dotenv
|
|
18
|
+
DOTENV_AVAILABLE = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
DOTENV_AVAILABLE = False
|
|
21
|
+
|
|
22
|
+
def load_env_config(
|
|
23
|
+
base_url: Optional[str] = None,
|
|
24
|
+
username: Optional[str] = None,
|
|
25
|
+
password: Optional[str] = None,
|
|
26
|
+
verify_ssl: Optional[Union[bool, str]] = None,
|
|
27
|
+
load_env: bool = True,
|
|
28
|
+
env_file: Optional[str] = None
|
|
29
|
+
) -> Dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Load configuration from environment variables and .env files.
|
|
32
|
+
|
|
33
|
+
Priority: explicit parameters > environment variables > defaults
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
base_url: Explicit base URL (overrides env)
|
|
37
|
+
username: Explicit username (overrides env)
|
|
38
|
+
password: Explicit password (overrides env)
|
|
39
|
+
verify_ssl: SSL verification - True/False or path to certificate bundle (overrides env)
|
|
40
|
+
load_env: Whether to load from environment/.env file
|
|
41
|
+
env_file: Specific .env file path
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dict with resolved configuration values
|
|
45
|
+
"""
|
|
46
|
+
# Load .env file if available and requested
|
|
47
|
+
if load_env and DOTENV_AVAILABLE:
|
|
48
|
+
if env_file:
|
|
49
|
+
load_dotenv(env_file)
|
|
50
|
+
else:
|
|
51
|
+
load_dotenv() # Loads .env from current directory
|
|
52
|
+
|
|
53
|
+
# Priority: explicit parameters > environment variables > defaults
|
|
54
|
+
resolved_base_url = base_url or os.getenv('ALFRESCO_URL') or os.getenv('ALFRESCO_BASE_URL') or 'http://localhost:8080'
|
|
55
|
+
resolved_username = username or os.getenv('ALFRESCO_USERNAME') or 'admin'
|
|
56
|
+
resolved_password = password or os.getenv('ALFRESCO_PASSWORD') or 'admin'
|
|
57
|
+
|
|
58
|
+
# Handle SSL verification (supports bool or certificate path like raw client)
|
|
59
|
+
if verify_ssl is not None:
|
|
60
|
+
resolved_verify_ssl = verify_ssl
|
|
61
|
+
else:
|
|
62
|
+
ssl_env = os.getenv('ALFRESCO_VERIFY_SSL') or os.getenv('ALFRESCO_SSL_VERIFY') or 'true'
|
|
63
|
+
# Support raw client SSL modes: True, False, or certificate path
|
|
64
|
+
if ssl_env.lower() in ('false', '0', 'no', 'off'):
|
|
65
|
+
resolved_verify_ssl = False
|
|
66
|
+
elif ssl_env.lower() in ('true', '1', 'yes', 'on'):
|
|
67
|
+
resolved_verify_ssl = True
|
|
68
|
+
else:
|
|
69
|
+
# Assume it's a certificate path
|
|
70
|
+
resolved_verify_ssl = ssl_env
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
'base_url': resolved_base_url,
|
|
74
|
+
'username': resolved_username,
|
|
75
|
+
'password': resolved_password,
|
|
76
|
+
'verify_ssl': resolved_verify_ssl
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class SimpleAuthUtil:
|
|
80
|
+
"""
|
|
81
|
+
Simple authentication utility using Basic authentication.
|
|
82
|
+
|
|
83
|
+
This is the proven working implementation that the MCP server uses.
|
|
84
|
+
Provides reliable Basic authentication for inheritance-based clients.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, username: str, password: str):
|
|
88
|
+
"""
|
|
89
|
+
Initialize simple auth utility.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
username: Alfresco username
|
|
93
|
+
password: Alfresco password
|
|
94
|
+
"""
|
|
95
|
+
self.username = username
|
|
96
|
+
self.password = password
|
|
97
|
+
|
|
98
|
+
def get_basic_auth_header(self):
|
|
99
|
+
"""Get basic auth header for HTTP requests."""
|
|
100
|
+
auth_string = f"{self.username}:{self.password}"
|
|
101
|
+
auth_b64 = base64.b64encode(auth_string.encode()).decode()
|
|
102
|
+
return f'Basic {auth_b64}'
|
|
103
|
+
|
|
104
|
+
def get_auth_token(self):
|
|
105
|
+
"""Get just the base64 auth token (without 'Basic ' prefix) for AuthenticatedClient."""
|
|
106
|
+
auth_string = f"{self.username}:{self.password}"
|
|
107
|
+
return base64.b64encode(auth_string.encode()).decode()
|
|
108
|
+
|
|
109
|
+
def get_auth_prefix(self):
|
|
110
|
+
"""Get the authentication prefix for this auth type."""
|
|
111
|
+
return "Basic"
|
|
112
|
+
|
|
113
|
+
def is_authenticated(self):
|
|
114
|
+
return True # Simple auth is always "authenticated"
|
|
115
|
+
|
|
116
|
+
async def ensure_authenticated(self):
|
|
117
|
+
return True # Simple auth is always ready
|
|
11
118
|
|
|
12
119
|
class AuthUtil:
|
|
13
120
|
"""
|
|
@@ -22,7 +129,7 @@ class AuthUtil:
|
|
|
22
129
|
base_url: str,
|
|
23
130
|
username: str,
|
|
24
131
|
password: str,
|
|
25
|
-
verify_ssl: bool = True,
|
|
132
|
+
verify_ssl: Union[bool, str] = True,
|
|
26
133
|
timeout: int = 30
|
|
27
134
|
):
|
|
28
135
|
"""
|
|
@@ -32,7 +139,7 @@ class AuthUtil:
|
|
|
32
139
|
base_url: Base URL of Alfresco instance
|
|
33
140
|
username: Alfresco username
|
|
34
141
|
password: Alfresco password
|
|
35
|
-
verify_ssl:
|
|
142
|
+
verify_ssl: SSL verification - True, False, or path to certificate bundle
|
|
36
143
|
timeout: Request timeout in seconds
|
|
37
144
|
"""
|
|
38
145
|
self.base_url = base_url.rstrip('/')
|
|
@@ -47,28 +154,32 @@ class AuthUtil:
|
|
|
47
154
|
|
|
48
155
|
async def authenticate(self) -> bool:
|
|
49
156
|
"""
|
|
50
|
-
Authenticate with Alfresco and get ticket.
|
|
157
|
+
Authenticate with Alfresco and get ticket using direct HTTP request.
|
|
158
|
+
|
|
159
|
+
This uses the WORKING authentication method discovered in test_working_api.py.
|
|
160
|
+
Uses direct HTTP requests instead of raw clients to avoid header auth issues.
|
|
51
161
|
|
|
52
162
|
Returns:
|
|
53
163
|
True if authentication successful, False otherwise
|
|
54
164
|
"""
|
|
55
165
|
try:
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
auth_client = AlfrescoAuthClient(self.base_url, None, self.verify_ssl, self.timeout)
|
|
61
|
-
|
|
62
|
-
ticket_body = TicketBody(userId=self.username, password=self.password)
|
|
63
|
-
ticket_response = await auth_client.create_ticket(ticket_body)
|
|
166
|
+
# Use direct HTTP approach like the working test code
|
|
167
|
+
auth_url = f"{self.base_url}/alfresco/api/-default-/public/authentication/versions/1/tickets"
|
|
168
|
+
auth_data = {"userId": self.username, "password": self.password}
|
|
64
169
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Tickets typically expire after 1 hour
|
|
68
|
-
self.ticket_expires = datetime.now() + timedelta(hours=1)
|
|
69
|
-
self._authenticated = True
|
|
70
|
-
return True
|
|
170
|
+
async with httpx.AsyncClient(verify=self.verify_ssl, timeout=self.timeout) as client:
|
|
171
|
+
response = await client.post(auth_url, json=auth_data)
|
|
71
172
|
|
|
173
|
+
if response.status_code == 201:
|
|
174
|
+
ticket_data = response.json()
|
|
175
|
+
self.ticket = ticket_data["entry"]["id"]
|
|
176
|
+
# Tickets typically expire after 1 hour
|
|
177
|
+
self.ticket_expires = datetime.now() + timedelta(hours=1)
|
|
178
|
+
self._authenticated = True
|
|
179
|
+
return True
|
|
180
|
+
else:
|
|
181
|
+
print(f"Authentication failed with status {response.status_code}: {response.text}")
|
|
182
|
+
|
|
72
183
|
except Exception as e:
|
|
73
184
|
print(f"Authentication failed: {e}")
|
|
74
185
|
self._authenticated = False
|
|
@@ -88,7 +199,7 @@ class AuthUtil:
|
|
|
88
199
|
|
|
89
200
|
def get_auth_headers(self) -> Dict[str, str]:
|
|
90
201
|
"""Get authentication headers for API requests"""
|
|
91
|
-
if not self.is_authenticated():
|
|
202
|
+
if not self.is_authenticated() or not self.ticket:
|
|
92
203
|
return {}
|
|
93
204
|
|
|
94
205
|
return {
|
|
@@ -101,3 +212,423 @@ class AuthUtil:
|
|
|
101
212
|
return True
|
|
102
213
|
|
|
103
214
|
return await self.authenticate()
|
|
215
|
+
|
|
216
|
+
def get_basic_auth_header(self):
|
|
217
|
+
"""Get basic auth header for compatibility with SimpleAuthUtil interface"""
|
|
218
|
+
auth_string = f"{self.username}:{self.password}"
|
|
219
|
+
auth_b64 = base64.b64encode(auth_string.encode()).decode()
|
|
220
|
+
return f'Basic {auth_b64}'
|
|
221
|
+
|
|
222
|
+
def get_auth_token(self):
|
|
223
|
+
"""Get just the base64 auth token (without 'Basic ' prefix) for AuthenticatedClient."""
|
|
224
|
+
auth_string = f"{self.username}:{self.password}"
|
|
225
|
+
return base64.b64encode(auth_string.encode()).decode()
|
|
226
|
+
|
|
227
|
+
def get_auth_prefix(self):
|
|
228
|
+
"""Get authentication prefix for headers. AuthUtil uses Basic auth."""
|
|
229
|
+
return "Basic"
|
|
230
|
+
|
|
231
|
+
def add_auth_params(self, url: str) -> str:
|
|
232
|
+
"""
|
|
233
|
+
Add authentication parameters to URL (query parameter method).
|
|
234
|
+
|
|
235
|
+
This is the WORKING authentication method for this Alfresco instance.
|
|
236
|
+
Uses alf_ticket={ticket} as query parameter instead of headers.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
url: Base URL to add authentication to
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
URL with authentication parameters added
|
|
243
|
+
"""
|
|
244
|
+
if not self.is_authenticated() or not self.ticket:
|
|
245
|
+
# No authentication available, return original URL
|
|
246
|
+
return url
|
|
247
|
+
|
|
248
|
+
# Add ticket as query parameter
|
|
249
|
+
separator = "&" if "?" in url else "?"
|
|
250
|
+
return f"{url}{separator}alf_ticket={self.ticket}"
|
|
251
|
+
|
|
252
|
+
class TicketAuthUtil:
|
|
253
|
+
"""
|
|
254
|
+
Enhanced authentication utility with direct ticket support.
|
|
255
|
+
Handles both Basic auth and ticket-based authentication.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
def __init__(self, username: str, password: str, base_url: str = ""):
|
|
259
|
+
"""
|
|
260
|
+
Initialize ticket auth utility.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
username: Alfresco username
|
|
264
|
+
password: Alfresco password
|
|
265
|
+
base_url: Base URL for Alfresco (optional, for future ticket API calls)
|
|
266
|
+
"""
|
|
267
|
+
self.username = username
|
|
268
|
+
self.password = password
|
|
269
|
+
self.base_url = base_url.rstrip('/')
|
|
270
|
+
self.ticket = None
|
|
271
|
+
self._authenticated = False
|
|
272
|
+
|
|
273
|
+
def get_basic_auth_header(self):
|
|
274
|
+
"""Get basic auth header for initial authentication."""
|
|
275
|
+
auth_string = f"{self.username}:{self.password}"
|
|
276
|
+
auth_b64 = base64.b64encode(auth_string.encode()).decode()
|
|
277
|
+
return f'Basic {auth_b64}'
|
|
278
|
+
|
|
279
|
+
def get_auth_token(self):
|
|
280
|
+
"""Get just the base64 auth token (without 'Basic ' prefix) for AuthenticatedClient."""
|
|
281
|
+
auth_string = f"{self.username}:{self.password}"
|
|
282
|
+
return base64.b64encode(auth_string.encode()).decode()
|
|
283
|
+
|
|
284
|
+
def get_ticket_header(self):
|
|
285
|
+
"""Get ticket auth header if we have a ticket."""
|
|
286
|
+
if self.ticket:
|
|
287
|
+
return f'Basic {base64.b64encode(self.ticket.encode()).decode()}'
|
|
288
|
+
return self.get_basic_auth_header()
|
|
289
|
+
|
|
290
|
+
def get_auth_header(self):
|
|
291
|
+
"""Get the appropriate auth header (ticket preferred, basic fallback)."""
|
|
292
|
+
return self.get_ticket_header()
|
|
293
|
+
|
|
294
|
+
def get_auth_prefix(self):
|
|
295
|
+
"""Get authentication prefix for headers. TicketAuthUtil uses Basic auth."""
|
|
296
|
+
return "Basic"
|
|
297
|
+
|
|
298
|
+
def store_ticket_from_response(self, response_data):
|
|
299
|
+
"""
|
|
300
|
+
Store ticket from authentication response.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
response_data: Response from create_ticket API call
|
|
304
|
+
"""
|
|
305
|
+
if hasattr(response_data, 'entry') and hasattr(response_data.entry, 'id'):
|
|
306
|
+
self.ticket = response_data.entry.id
|
|
307
|
+
self._authenticated = True
|
|
308
|
+
elif isinstance(response_data, dict) and 'entry' in response_data:
|
|
309
|
+
if 'id' in response_data['entry']:
|
|
310
|
+
self.ticket = response_data['entry']['id']
|
|
311
|
+
self._authenticated = True
|
|
312
|
+
|
|
313
|
+
def is_authenticated(self):
|
|
314
|
+
"""Check if we have a valid ticket."""
|
|
315
|
+
return self._authenticated and self.ticket is not None
|
|
316
|
+
|
|
317
|
+
class OAuth2AuthUtil:
|
|
318
|
+
"""
|
|
319
|
+
OAuth2 authentication utility for enterprise environments.
|
|
320
|
+
|
|
321
|
+
Supports multiple OAuth2 flows while maintaining the same interface
|
|
322
|
+
as other auth utilities for seamless integration.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
def __init__(
|
|
326
|
+
self,
|
|
327
|
+
base_url: str,
|
|
328
|
+
client_id: str,
|
|
329
|
+
client_secret: Optional[str] = None,
|
|
330
|
+
token_endpoint: Optional[str] = None,
|
|
331
|
+
authorization_endpoint: Optional[str] = None,
|
|
332
|
+
# OAuth2 flow configuration
|
|
333
|
+
grant_type: str = "client_credentials", # or "authorization_code"
|
|
334
|
+
scope: Optional[str] = None,
|
|
335
|
+
redirect_uri: Optional[str] = None,
|
|
336
|
+
# Token management
|
|
337
|
+
access_token: Optional[str] = None,
|
|
338
|
+
refresh_token: Optional[str] = None,
|
|
339
|
+
# Standard auth util parameters
|
|
340
|
+
verify_ssl: Union[bool, str] = True,
|
|
341
|
+
timeout: int = 30,
|
|
342
|
+
# Environment loading
|
|
343
|
+
load_env: bool = True,
|
|
344
|
+
env_file: Optional[str] = None
|
|
345
|
+
):
|
|
346
|
+
"""
|
|
347
|
+
Initialize OAuth2 authentication utility.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
base_url: Alfresco server base URL
|
|
351
|
+
client_id: OAuth2 client identifier
|
|
352
|
+
client_secret: OAuth2 client secret (for confidential clients)
|
|
353
|
+
token_endpoint: OAuth2 token endpoint URL
|
|
354
|
+
authorization_endpoint: OAuth2 authorization endpoint URL (for auth code flow)
|
|
355
|
+
grant_type: OAuth2 grant type ("client_credentials", "authorization_code", "refresh_token")
|
|
356
|
+
scope: Requested OAuth2 scopes
|
|
357
|
+
redirect_uri: Redirect URI for authorization code flow
|
|
358
|
+
access_token: Existing access token (optional)
|
|
359
|
+
refresh_token: Existing refresh token (optional)
|
|
360
|
+
verify_ssl: SSL verification - True, False, or path to certificate bundle
|
|
361
|
+
timeout: Request timeout in seconds
|
|
362
|
+
load_env: Whether to load configuration from environment
|
|
363
|
+
env_file: Optional path to .env file
|
|
364
|
+
"""
|
|
365
|
+
# Load configuration from environment if needed
|
|
366
|
+
config = load_env_config(
|
|
367
|
+
base_url=base_url,
|
|
368
|
+
username=None, # Not used for OAuth2
|
|
369
|
+
password=None, # Not used for OAuth2
|
|
370
|
+
verify_ssl=verify_ssl,
|
|
371
|
+
load_env=load_env,
|
|
372
|
+
env_file=env_file
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
self.base_url = config['base_url'].rstrip('/')
|
|
376
|
+
self.client_id = client_id
|
|
377
|
+
self.client_secret = client_secret
|
|
378
|
+
self.verify_ssl = config['verify_ssl']
|
|
379
|
+
self.timeout = timeout
|
|
380
|
+
|
|
381
|
+
# OAuth2 flow configuration
|
|
382
|
+
self.grant_type = grant_type
|
|
383
|
+
self.scope = scope
|
|
384
|
+
self.redirect_uri = redirect_uri
|
|
385
|
+
|
|
386
|
+
# Token endpoints - smart defaults for common providers
|
|
387
|
+
self.token_endpoint = token_endpoint or self._detect_token_endpoint()
|
|
388
|
+
self.authorization_endpoint = authorization_endpoint
|
|
389
|
+
|
|
390
|
+
# Token state
|
|
391
|
+
self.access_token = access_token
|
|
392
|
+
self.refresh_token = refresh_token
|
|
393
|
+
self.token_expires = None
|
|
394
|
+
self._authenticated = False
|
|
395
|
+
|
|
396
|
+
# For environment variable loading
|
|
397
|
+
self._load_oauth_env_config(load_env, env_file)
|
|
398
|
+
|
|
399
|
+
def _load_oauth_env_config(self, load_env: bool, env_file: Optional[str]):
|
|
400
|
+
"""Load OAuth2-specific configuration from environment."""
|
|
401
|
+
if not load_env:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
if DOTENV_AVAILABLE:
|
|
405
|
+
if env_file:
|
|
406
|
+
from dotenv import load_dotenv
|
|
407
|
+
load_dotenv(env_file)
|
|
408
|
+
else:
|
|
409
|
+
from dotenv import load_dotenv
|
|
410
|
+
load_dotenv()
|
|
411
|
+
|
|
412
|
+
# Load OAuth2 environment variables
|
|
413
|
+
self.client_id = self.client_id or os.getenv('OAUTH2_CLIENT_ID') or os.getenv('ALFRESCO_OAUTH2_CLIENT_ID')
|
|
414
|
+
self.client_secret = self.client_secret or os.getenv('OAUTH2_CLIENT_SECRET') or os.getenv('ALFRESCO_OAUTH2_CLIENT_SECRET')
|
|
415
|
+
self.token_endpoint = self.token_endpoint or os.getenv('OAUTH2_TOKEN_ENDPOINT') or os.getenv('ALFRESCO_OAUTH2_TOKEN_ENDPOINT')
|
|
416
|
+
self.scope = self.scope or os.getenv('OAUTH2_SCOPE') or os.getenv('ALFRESCO_OAUTH2_SCOPE')
|
|
417
|
+
|
|
418
|
+
# Existing tokens from environment
|
|
419
|
+
self.access_token = self.access_token or os.getenv('OAUTH2_ACCESS_TOKEN') or os.getenv('ALFRESCO_OAUTH2_ACCESS_TOKEN')
|
|
420
|
+
self.refresh_token = self.refresh_token or os.getenv('OAUTH2_REFRESH_TOKEN') or os.getenv('ALFRESCO_OAUTH2_REFRESH_TOKEN')
|
|
421
|
+
|
|
422
|
+
def _detect_token_endpoint(self) -> Optional[str]:
|
|
423
|
+
"""Smart detection of token endpoints for common providers."""
|
|
424
|
+
if not self.base_url:
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
# Common Alfresco OAuth2 patterns
|
|
428
|
+
alfresco_patterns = [
|
|
429
|
+
f"{self.base_url}/alfresco/service/oauth2/token",
|
|
430
|
+
f"{self.base_url}/auth/oauth/token",
|
|
431
|
+
f"{self.base_url}/oauth2/token"
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
# For cloud/enterprise environments, often separate auth servers
|
|
435
|
+
if "cloud" in self.base_url.lower() or "enterprise" in self.base_url.lower():
|
|
436
|
+
return None # Require explicit configuration
|
|
437
|
+
|
|
438
|
+
# Return most common pattern for on-premise
|
|
439
|
+
return alfresco_patterns[0]
|
|
440
|
+
|
|
441
|
+
async def authenticate(self) -> bool:
|
|
442
|
+
"""
|
|
443
|
+
Authenticate using OAuth2 flow and obtain access token.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
True if authentication successful, False otherwise
|
|
447
|
+
"""
|
|
448
|
+
try:
|
|
449
|
+
if self.grant_type == "client_credentials":
|
|
450
|
+
return await self._client_credentials_flow()
|
|
451
|
+
elif self.grant_type == "refresh_token" and self.refresh_token:
|
|
452
|
+
return await self._refresh_token_flow()
|
|
453
|
+
elif self.grant_type == "authorization_code":
|
|
454
|
+
# This typically requires user interaction, so we just validate existing token
|
|
455
|
+
return self.is_authenticated()
|
|
456
|
+
else:
|
|
457
|
+
raise ValueError(f"Unsupported grant type: {self.grant_type}")
|
|
458
|
+
|
|
459
|
+
except Exception as e:
|
|
460
|
+
print(f"OAuth2 authentication failed: {e}")
|
|
461
|
+
self._authenticated = False
|
|
462
|
+
return False
|
|
463
|
+
|
|
464
|
+
async def _client_credentials_flow(self) -> bool:
|
|
465
|
+
"""Execute OAuth2 client credentials flow."""
|
|
466
|
+
if not self.client_id or not self.client_secret or not self.token_endpoint:
|
|
467
|
+
raise ValueError(
|
|
468
|
+
"Client credentials flow requires client_id, client_secret, and token_endpoint"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
data = {
|
|
472
|
+
"grant_type": "client_credentials",
|
|
473
|
+
"client_id": self.client_id,
|
|
474
|
+
"client_secret": self.client_secret
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if self.scope:
|
|
478
|
+
data["scope"] = self.scope
|
|
479
|
+
|
|
480
|
+
async with httpx.AsyncClient(verify=self.verify_ssl, timeout=self.timeout) as client:
|
|
481
|
+
response = await client.post(
|
|
482
|
+
self.token_endpoint,
|
|
483
|
+
data=data,
|
|
484
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
if response.status_code == 200:
|
|
488
|
+
token_data = response.json()
|
|
489
|
+
self.access_token = token_data.get("access_token")
|
|
490
|
+
|
|
491
|
+
# Handle token expiration
|
|
492
|
+
expires_in = token_data.get("expires_in")
|
|
493
|
+
if expires_in:
|
|
494
|
+
self.token_expires = datetime.now() + timedelta(seconds=expires_in)
|
|
495
|
+
|
|
496
|
+
# Store refresh token if provided
|
|
497
|
+
if "refresh_token" in token_data:
|
|
498
|
+
self.refresh_token = token_data["refresh_token"]
|
|
499
|
+
|
|
500
|
+
self._authenticated = True
|
|
501
|
+
return True
|
|
502
|
+
|
|
503
|
+
return False
|
|
504
|
+
|
|
505
|
+
async def _refresh_token_flow(self) -> bool:
|
|
506
|
+
"""Refresh access token using refresh token."""
|
|
507
|
+
if not self.refresh_token or not self.token_endpoint:
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
data = {
|
|
511
|
+
"grant_type": "refresh_token",
|
|
512
|
+
"refresh_token": self.refresh_token,
|
|
513
|
+
"client_id": self.client_id
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if self.client_secret:
|
|
517
|
+
data["client_secret"] = self.client_secret
|
|
518
|
+
|
|
519
|
+
async with httpx.AsyncClient(verify=self.verify_ssl, timeout=self.timeout) as client:
|
|
520
|
+
response = await client.post(
|
|
521
|
+
self.token_endpoint,
|
|
522
|
+
data=data,
|
|
523
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
if response.status_code == 200:
|
|
527
|
+
token_data = response.json()
|
|
528
|
+
self.access_token = token_data.get("access_token")
|
|
529
|
+
|
|
530
|
+
# Update expiration
|
|
531
|
+
expires_in = token_data.get("expires_in")
|
|
532
|
+
if expires_in:
|
|
533
|
+
self.token_expires = datetime.now() + timedelta(seconds=expires_in)
|
|
534
|
+
|
|
535
|
+
# Update refresh token if provided
|
|
536
|
+
if "refresh_token" in token_data:
|
|
537
|
+
self.refresh_token = token_data["refresh_token"]
|
|
538
|
+
|
|
539
|
+
self._authenticated = True
|
|
540
|
+
return True
|
|
541
|
+
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
def is_authenticated(self) -> bool:
|
|
545
|
+
"""Check if currently authenticated with valid access token."""
|
|
546
|
+
if not self._authenticated or not self.access_token:
|
|
547
|
+
return False
|
|
548
|
+
|
|
549
|
+
# Check token expiration
|
|
550
|
+
if self.token_expires and datetime.now() >= self.token_expires:
|
|
551
|
+
self._authenticated = False
|
|
552
|
+
return False
|
|
553
|
+
|
|
554
|
+
return True
|
|
555
|
+
|
|
556
|
+
def get_auth_headers(self) -> Dict[str, str]:
|
|
557
|
+
"""Get authentication headers for API requests."""
|
|
558
|
+
if not self.is_authenticated():
|
|
559
|
+
return {}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
"Authorization": f"Bearer {self.access_token}"
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
def get_basic_auth_header(self):
|
|
566
|
+
"""Compatibility method - returns Bearer token as auth header."""
|
|
567
|
+
if self.is_authenticated():
|
|
568
|
+
return f"Bearer {self.access_token}"
|
|
569
|
+
return ""
|
|
570
|
+
|
|
571
|
+
def get_auth_token(self):
|
|
572
|
+
"""Get just the access token (without 'Bearer ' prefix) for AuthenticatedClient."""
|
|
573
|
+
if self.is_authenticated():
|
|
574
|
+
return self.access_token
|
|
575
|
+
return ""
|
|
576
|
+
|
|
577
|
+
def get_auth_prefix(self):
|
|
578
|
+
"""Get authentication prefix for headers. OAuth2AuthUtil uses Bearer tokens."""
|
|
579
|
+
return "Bearer"
|
|
580
|
+
|
|
581
|
+
async def ensure_authenticated(self) -> bool:
|
|
582
|
+
"""Ensure we have valid authentication, refresh if needed."""
|
|
583
|
+
if self.is_authenticated():
|
|
584
|
+
return True
|
|
585
|
+
|
|
586
|
+
# Try refresh token first if available
|
|
587
|
+
if self.refresh_token:
|
|
588
|
+
if await self._refresh_token_flow():
|
|
589
|
+
return True
|
|
590
|
+
|
|
591
|
+
# Fall back to main authentication flow
|
|
592
|
+
return await self.authenticate()
|
|
593
|
+
|
|
594
|
+
@classmethod
|
|
595
|
+
def from_env(
|
|
596
|
+
cls,
|
|
597
|
+
base_url: Optional[str] = None,
|
|
598
|
+
env_file: Optional[str] = None,
|
|
599
|
+
grant_type: str = "client_credentials",
|
|
600
|
+
**kwargs
|
|
601
|
+
):
|
|
602
|
+
"""
|
|
603
|
+
Create OAuth2AuthUtil from environment variables.
|
|
604
|
+
|
|
605
|
+
Expected environment variables:
|
|
606
|
+
- OAUTH2_CLIENT_ID or ALFRESCO_OAUTH2_CLIENT_ID
|
|
607
|
+
- OAUTH2_CLIENT_SECRET or ALFRESCO_OAUTH2_CLIENT_SECRET
|
|
608
|
+
- OAUTH2_TOKEN_ENDPOINT or ALFRESCO_OAUTH2_TOKEN_ENDPOINT
|
|
609
|
+
- OAUTH2_SCOPE or ALFRESCO_OAUTH2_SCOPE (optional)
|
|
610
|
+
"""
|
|
611
|
+
return cls(
|
|
612
|
+
base_url=base_url or os.getenv('ALFRESCO_URL') or os.getenv('ALFRESCO_BASE_URL') or 'http://localhost:8080',
|
|
613
|
+
client_id="", # Will be loaded from env
|
|
614
|
+
grant_type=grant_type,
|
|
615
|
+
load_env=True,
|
|
616
|
+
env_file=env_file,
|
|
617
|
+
**kwargs
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
def get_config_info(self) -> Dict[str, Any]:
|
|
621
|
+
"""Get configuration information for debugging (without sensitive data)."""
|
|
622
|
+
return {
|
|
623
|
+
"base_url": self.base_url,
|
|
624
|
+
"client_id": self.client_id[:8] + "..." if self.client_id else None,
|
|
625
|
+
"client_secret": "***" if self.client_secret else None,
|
|
626
|
+
"token_endpoint": self.token_endpoint,
|
|
627
|
+
"grant_type": self.grant_type,
|
|
628
|
+
"scope": self.scope,
|
|
629
|
+
"has_access_token": bool(self.access_token),
|
|
630
|
+
"has_refresh_token": bool(self.refresh_token),
|
|
631
|
+
"token_expires": self.token_expires.isoformat() if self.token_expires else None,
|
|
632
|
+
"is_authenticated": self.is_authenticated(),
|
|
633
|
+
"verify_ssl": self.verify_ssl
|
|
634
|
+
}
|