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.
Files changed (168) hide show
  1. python_alfresco_api/__init__.py +23 -10
  2. python_alfresco_api/auth_util.py +550 -19
  3. python_alfresco_api/client_factory.py +158 -39
  4. python_alfresco_api/clients/__init__.py +60 -1
  5. python_alfresco_api/clients/auth/__init__.py +14 -0
  6. python_alfresco_api/clients/auth/auth_client.py +167 -0
  7. python_alfresco_api/clients/auth/authentication/__init__.py +10 -0
  8. python_alfresco_api/clients/auth/authentication/authentication_client.py +94 -0
  9. python_alfresco_api/clients/auth/authentication/models.py +80 -0
  10. python_alfresco_api/clients/auth/models.py +55 -0
  11. python_alfresco_api/clients/conversion_utils.py +197 -0
  12. python_alfresco_api/clients/core/__init__.py +11 -0
  13. python_alfresco_api/clients/core/actions/__init__.py +10 -0
  14. python_alfresco_api/clients/core/actions/actions_client.py +320 -0
  15. python_alfresco_api/clients/core/actions/models.py +84 -0
  16. python_alfresco_api/clients/core/activities/__init__.py +10 -0
  17. python_alfresco_api/clients/core/activities/activities_client.py +141 -0
  18. python_alfresco_api/clients/core/activities/models.py +84 -0
  19. python_alfresco_api/clients/core/audit/__init__.py +10 -0
  20. python_alfresco_api/clients/core/audit/audit_client.py +95 -0
  21. python_alfresco_api/clients/core/audit/models.py +84 -0
  22. python_alfresco_api/clients/core/comments/__init__.py +10 -0
  23. python_alfresco_api/clients/core/comments/comments_client.py +320 -0
  24. python_alfresco_api/clients/core/comments/models.py +84 -0
  25. python_alfresco_api/clients/core/content/__init__.py +15 -0
  26. python_alfresco_api/clients/core/content/content_client.py +471 -0
  27. python_alfresco_api/clients/core/content/models.py +139 -0
  28. python_alfresco_api/clients/core/core_client.py +440 -0
  29. python_alfresco_api/clients/core/favorites/__init__.py +10 -0
  30. python_alfresco_api/clients/core/favorites/favorites_client.py +87 -0
  31. python_alfresco_api/clients/core/favorites/models.py +84 -0
  32. python_alfresco_api/clients/core/groups/__init__.py +10 -0
  33. python_alfresco_api/clients/core/groups/groups_client.py +87 -0
  34. python_alfresco_api/clients/core/groups/models.py +84 -0
  35. python_alfresco_api/clients/core/models.py +133 -0
  36. python_alfresco_api/clients/core/networks/__init__.py +10 -0
  37. python_alfresco_api/clients/core/networks/models.py +84 -0
  38. python_alfresco_api/clients/core/networks/networks_client.py +77 -0
  39. python_alfresco_api/clients/core/nodes/__init__.py +34 -0
  40. python_alfresco_api/clients/core/nodes/copy_node.py +282 -0
  41. python_alfresco_api/clients/core/nodes/create_association.py +222 -0
  42. python_alfresco_api/clients/core/nodes/create_folder.py +336 -0
  43. python_alfresco_api/clients/core/nodes/create_node.py +379 -0
  44. python_alfresco_api/clients/core/nodes/create_secondary_child_association.py +182 -0
  45. python_alfresco_api/clients/core/nodes/delete_association.py +145 -0
  46. python_alfresco_api/clients/core/nodes/delete_node.py +135 -0
  47. python_alfresco_api/clients/core/nodes/delete_secondary_child_association.py +145 -0
  48. python_alfresco_api/clients/core/nodes/get_node.py +246 -0
  49. python_alfresco_api/clients/core/nodes/list_node_children.py +304 -0
  50. python_alfresco_api/clients/core/nodes/list_parents.py +227 -0
  51. python_alfresco_api/clients/core/nodes/list_secondary_children.py +253 -0
  52. python_alfresco_api/clients/core/nodes/list_source_associations.py +283 -0
  53. python_alfresco_api/clients/core/nodes/list_target_associations.py +225 -0
  54. python_alfresco_api/clients/core/nodes/lock_node.py +237 -0
  55. python_alfresco_api/clients/core/nodes/models.py +265 -0
  56. python_alfresco_api/clients/core/nodes/move_node.py +282 -0
  57. python_alfresco_api/clients/core/nodes/nodes_client.py +436 -0
  58. python_alfresco_api/clients/core/nodes/unlock_node.py +197 -0
  59. python_alfresco_api/clients/core/nodes/update_node.py +297 -0
  60. python_alfresco_api/clients/core/nodes/update_node_content.py +276 -0
  61. python_alfresco_api/clients/core/people/__init__.py +10 -0
  62. python_alfresco_api/clients/core/people/models.py +84 -0
  63. python_alfresco_api/clients/core/people/people_client.py +83 -0
  64. python_alfresco_api/clients/core/preferences/__init__.py +10 -0
  65. python_alfresco_api/clients/core/preferences/models.py +84 -0
  66. python_alfresco_api/clients/core/preferences/preferences_client.py +75 -0
  67. python_alfresco_api/clients/core/probes/__init__.py +10 -0
  68. python_alfresco_api/clients/core/probes/models.py +84 -0
  69. python_alfresco_api/clients/core/probes/probes_client.py +102 -0
  70. python_alfresco_api/clients/core/queries/__init__.py +10 -0
  71. python_alfresco_api/clients/core/queries/models.py +84 -0
  72. python_alfresco_api/clients/core/queries/queries_client.py +278 -0
  73. python_alfresco_api/clients/core/ratings/__init__.py +10 -0
  74. python_alfresco_api/clients/core/ratings/models.py +84 -0
  75. python_alfresco_api/clients/core/ratings/ratings_client.py +255 -0
  76. python_alfresco_api/clients/core/renditions/__init__.py +10 -0
  77. python_alfresco_api/clients/core/renditions/models.py +84 -0
  78. python_alfresco_api/clients/core/renditions/renditions_client.py +170 -0
  79. python_alfresco_api/clients/core/shared_links/__init__.py +10 -0
  80. python_alfresco_api/clients/core/shared_links/models.py +84 -0
  81. python_alfresco_api/clients/core/shared_links/shared_links_client.py +89 -0
  82. python_alfresco_api/clients/core/sites/__init__.py +10 -0
  83. python_alfresco_api/clients/core/sites/models.py +84 -0
  84. python_alfresco_api/clients/core/sites/sites_client.py +113 -0
  85. python_alfresco_api/clients/core/tags/__init__.py +10 -0
  86. python_alfresco_api/clients/core/tags/models.py +84 -0
  87. python_alfresco_api/clients/core/tags/tags_client.py +83 -0
  88. python_alfresco_api/clients/core/trashcan/__init__.py +10 -0
  89. python_alfresco_api/clients/core/trashcan/models.py +84 -0
  90. python_alfresco_api/clients/core/trashcan/trashcan_client.py +87 -0
  91. python_alfresco_api/clients/core/versions/__init__.py +10 -0
  92. python_alfresco_api/clients/core/versions/models.py +117 -0
  93. python_alfresco_api/clients/core/versions/versions_client.py +412 -0
  94. python_alfresco_api/clients/discovery/__init__.py +14 -0
  95. python_alfresco_api/clients/discovery/discovery/__init__.py +12 -0
  96. python_alfresco_api/clients/discovery/discovery/discovery_operations.py +156 -0
  97. python_alfresco_api/clients/discovery/discovery/models.py +80 -0
  98. python_alfresco_api/clients/discovery/discovery_client.py +151 -0
  99. python_alfresco_api/clients/discovery/models.py +55 -0
  100. python_alfresco_api/clients/field_mapping.py +297 -0
  101. python_alfresco_api/clients/master_client.py +309 -0
  102. python_alfresco_api/clients/model/__init__.py +14 -0
  103. python_alfresco_api/clients/model/aspects/__init__.py +10 -0
  104. python_alfresco_api/clients/model/aspects/aspects_client.py +87 -0
  105. python_alfresco_api/clients/model/aspects/models.py +80 -0
  106. python_alfresco_api/clients/model/model_client.py +165 -0
  107. python_alfresco_api/clients/model/models.py +55 -0
  108. python_alfresco_api/clients/model/types/__init__.py +10 -0
  109. python_alfresco_api/clients/model/types/models.py +80 -0
  110. python_alfresco_api/clients/model/types/types_client.py +87 -0
  111. python_alfresco_api/clients/models.py +110 -0
  112. python_alfresco_api/clients/search/__init__.py +14 -0
  113. python_alfresco_api/clients/search/models.py +55 -0
  114. python_alfresco_api/clients/search/search/__init__.py +12 -0
  115. python_alfresco_api/clients/search/search/models.py +80 -0
  116. python_alfresco_api/clients/search/search/search_operations.py +157 -0
  117. python_alfresco_api/clients/search/search_client.py +151 -0
  118. python_alfresco_api/clients/search_sql/__init__.py +14 -0
  119. python_alfresco_api/clients/search_sql/models.py +55 -0
  120. python_alfresco_api/clients/search_sql/search_sql_client.py +150 -0
  121. python_alfresco_api/clients/search_sql/sql/__init__.py +10 -0
  122. python_alfresco_api/clients/search_sql/sql/models.py +80 -0
  123. python_alfresco_api/clients/search_sql/sql/sql_client.py +89 -0
  124. python_alfresco_api/clients/workflow/__init__.py +14 -0
  125. python_alfresco_api/clients/workflow/deployments/__init__.py +10 -0
  126. python_alfresco_api/clients/workflow/deployments/deployments_client.py +85 -0
  127. python_alfresco_api/clients/workflow/deployments/models.py +80 -0
  128. python_alfresco_api/clients/workflow/models.py +55 -0
  129. python_alfresco_api/clients/workflow/process_definitions/__init__.py +16 -0
  130. python_alfresco_api/clients/workflow/process_definitions/models.py +80 -0
  131. python_alfresco_api/clients/workflow/process_definitions/process_definitions_client.py +401 -0
  132. python_alfresco_api/clients/workflow/processes/__init__.py +16 -0
  133. python_alfresco_api/clients/workflow/processes/models.py +80 -0
  134. python_alfresco_api/clients/workflow/processes/processes_client.py +999 -0
  135. python_alfresco_api/clients/workflow/tasks/__init__.py +16 -0
  136. python_alfresco_api/clients/workflow/tasks/models.py +80 -0
  137. python_alfresco_api/clients/workflow/tasks/tasks_client.py +486 -0
  138. python_alfresco_api/clients/workflow/workflow_client.py +193 -0
  139. python_alfresco_api/models/alfresco_core_models.py +1288 -1288
  140. python_alfresco_api/raw_clients/alfresco_core_client/core_client/api/nodes/create_node.py +1327 -1327
  141. python_alfresco_api/raw_clients/alfresco_core_client/core_client/models/association.py +67 -67
  142. python_alfresco_api/raw_clients/alfresco_core_client/core_client/models/node.py +282 -282
  143. python_alfresco_api/raw_clients/alfresco_core_client/core_client/models/node_association.py +300 -300
  144. python_alfresco_api/utils/__init__.py +73 -0
  145. python_alfresco_api/utils/content_utils.py +289 -0
  146. python_alfresco_api/utils/content_utils_highlevel.py +393 -0
  147. python_alfresco_api/utils/mcp_formatters.py +426 -0
  148. python_alfresco_api/utils/node_utils.py +469 -0
  149. python_alfresco_api/utils/node_utils_highlevel.py +280 -0
  150. python_alfresco_api/utils/search_utils.py +445 -0
  151. python_alfresco_api/utils/version_utils.py +373 -0
  152. python_alfresco_api/utils/version_utils_highlevel.py +565 -0
  153. python_alfresco_api-1.1.0.dist-info/METADATA +652 -0
  154. {python_alfresco_api-1.0.2.dist-info → python_alfresco_api-1.1.0.dist-info}/RECORD +156 -24
  155. python_alfresco_api/clients/auth_client.py +0 -85
  156. python_alfresco_api/clients/core_client.py +0 -85
  157. python_alfresco_api/clients/discovery_client.py +0 -85
  158. python_alfresco_api/clients/model_client.py +0 -85
  159. python_alfresco_api/clients/search_client.py +0 -85
  160. python_alfresco_api/clients/search_sql_client.py +0 -85
  161. python_alfresco_api/clients/workflow_client.py +0 -85
  162. python_alfresco_api/examples/basic_usage.py +0 -52
  163. python_alfresco_api/examples/llm_integration.py +0 -155
  164. python_alfresco_api/tests/__init__.py +0 -1
  165. python_alfresco_api/tests/test_basic.py +0 -115
  166. python_alfresco_api-1.0.2.dist-info/METADATA +0 -796
  167. {python_alfresco_api-1.0.2.dist-info → python_alfresco_api-1.1.0.dist-info}/WHEEL +0 -0
  168. {python_alfresco_api-1.0.2.dist-info → python_alfresco_api-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -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.auth_client import AlfrescoAuthClient
19
- from .clients.core_client import AlfrescoCoreClient
20
- from .clients.discovery_client import AlfrescoDiscoveryClient
21
- from .clients.search_client import AlfrescoSearchClient
22
- from .clients.workflow_client import AlfrescoWorkflowClient
23
- from .clients.model_client import AlfrescoModelClient
24
- from .clients.search_sql_client import AlfrescoSearchSqlClient
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
  ]
@@ -6,8 +6,115 @@ Handles ticket-based authentication with automatic renewal.
6
6
  """
7
7
 
8
8
  import asyncio
9
- from typing import Optional, Dict, Any
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: Whether to verify SSL certificates
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
- # Import here to avoid circular imports
57
- from .clients.auth_client import AlfrescoAuthClient
58
- from .models.alfresco_auth_models import TicketBody
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
- if ticket_response and hasattr(ticket_response, 'entry'):
66
- self.ticket = ticket_response.entry.id
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
+ }