cloud-dog-api-kit 0.13.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.
- cloud_dog_api_kit/__init__.py +170 -0
- cloud_dog_api_kit/a2a/__init__.py +53 -0
- cloud_dog_api_kit/a2a/card.py +138 -0
- cloud_dog_api_kit/a2a/events.py +1123 -0
- cloud_dog_api_kit/a2a/gateway.py +105 -0
- cloud_dog_api_kit/a2a/skill_audit.py +107 -0
- cloud_dog_api_kit/auth/__init__.py +35 -0
- cloud_dog_api_kit/auth/dependency.py +121 -0
- cloud_dog_api_kit/auth/rbac.py +107 -0
- cloud_dog_api_kit/auth/service_auth.py +54 -0
- cloud_dog_api_kit/clients/__init__.py +29 -0
- cloud_dog_api_kit/clients/circuit_breaker.py +39 -0
- cloud_dog_api_kit/clients/http_client.py +127 -0
- cloud_dog_api_kit/clients/retry.py +83 -0
- cloud_dog_api_kit/compat/__init__.py +37 -0
- cloud_dog_api_kit/compat/envelope.py +120 -0
- cloud_dog_api_kit/compat/profile.py +102 -0
- cloud_dog_api_kit/compat/routes.py +90 -0
- cloud_dog_api_kit/config.py +54 -0
- cloud_dog_api_kit/correlation/__init__.py +50 -0
- cloud_dog_api_kit/correlation/context.py +118 -0
- cloud_dog_api_kit/correlation/middleware.py +133 -0
- cloud_dog_api_kit/envelopes/__init__.py +37 -0
- cloud_dog_api_kit/envelopes/error.py +87 -0
- cloud_dog_api_kit/envelopes/success.py +84 -0
- cloud_dog_api_kit/errors/__init__.py +51 -0
- cloud_dog_api_kit/errors/exceptions.py +184 -0
- cloud_dog_api_kit/errors/handler.py +102 -0
- cloud_dog_api_kit/errors/taxonomy.py +62 -0
- cloud_dog_api_kit/factory.py +157 -0
- cloud_dog_api_kit/idempotency/__init__.py +28 -0
- cloud_dog_api_kit/idempotency/middleware.py +118 -0
- cloud_dog_api_kit/idempotency/store.py +100 -0
- cloud_dog_api_kit/lifecycle/__init__.py +39 -0
- cloud_dog_api_kit/lifecycle/hooks.py +75 -0
- cloud_dog_api_kit/lifecycle/shutdown.py +178 -0
- cloud_dog_api_kit/mcp/__init__.py +122 -0
- cloud_dog_api_kit/mcp/async_jobs.py +126 -0
- cloud_dog_api_kit/mcp/client_sdk.py +235 -0
- cloud_dog_api_kit/mcp/client_transport/__init__.py +47 -0
- cloud_dog_api_kit/mcp/client_transport/base.py +98 -0
- cloud_dog_api_kit/mcp/client_transport/exceptions.py +37 -0
- cloud_dog_api_kit/mcp/client_transport/http_jsonrpc.py +405 -0
- cloud_dog_api_kit/mcp/client_transport/legacy_sse.py +320 -0
- cloud_dog_api_kit/mcp/client_transport/stdio.py +322 -0
- cloud_dog_api_kit/mcp/client_transport/streamable_http.py +748 -0
- cloud_dog_api_kit/mcp/contract.py +113 -0
- cloud_dog_api_kit/mcp/error_mapper.py +84 -0
- cloud_dog_api_kit/mcp/gateway.py +117 -0
- cloud_dog_api_kit/mcp/legacy_sse.py +129 -0
- cloud_dog_api_kit/mcp/session.py +96 -0
- cloud_dog_api_kit/mcp/sync_handler.py +269 -0
- cloud_dog_api_kit/mcp/tool_audit.py +136 -0
- cloud_dog_api_kit/mcp/tool_router.py +180 -0
- cloud_dog_api_kit/mcp/transport.py +1041 -0
- cloud_dog_api_kit/middleware/__init__.py +39 -0
- cloud_dog_api_kit/middleware/cors.py +74 -0
- cloud_dog_api_kit/middleware/logging.py +98 -0
- cloud_dog_api_kit/middleware/request_size_limit.py +86 -0
- cloud_dog_api_kit/middleware/timeout.py +78 -0
- cloud_dog_api_kit/middleware/timing.py +52 -0
- cloud_dog_api_kit/openapi/__init__.py +30 -0
- cloud_dog_api_kit/openapi/customise.py +69 -0
- cloud_dog_api_kit/openapi/route.py +46 -0
- cloud_dog_api_kit/routers/__init__.py +41 -0
- cloud_dog_api_kit/routers/crud.py +173 -0
- cloud_dog_api_kit/routers/health.py +160 -0
- cloud_dog_api_kit/routers/jobs.py +69 -0
- cloud_dog_api_kit/routers/version.py +46 -0
- cloud_dog_api_kit/schemas/__init__.py +36 -0
- cloud_dog_api_kit/schemas/envelopes.py +37 -0
- cloud_dog_api_kit/schemas/filters.py +103 -0
- cloud_dog_api_kit/schemas/pagination.py +148 -0
- cloud_dog_api_kit/streaming/__init__.py +28 -0
- cloud_dog_api_kit/streaming/events.py +47 -0
- cloud_dog_api_kit/streaming/jsonl.py +68 -0
- cloud_dog_api_kit/streaming/sse.py +102 -0
- cloud_dog_api_kit/testing/__init__.py +46 -0
- cloud_dog_api_kit/testing/conformance.py +156 -0
- cloud_dog_api_kit/testing/fixtures.py +90 -0
- cloud_dog_api_kit/testing/flows/__init__.py +32 -0
- cloud_dog_api_kit/testing/flows/auth_flow.py +41 -0
- cloud_dog_api_kit/testing/flows/crud_flow.py +50 -0
- cloud_dog_api_kit/testing/flows/job_flow.py +42 -0
- cloud_dog_api_kit/testing/flows/streaming_flow.py +42 -0
- cloud_dog_api_kit/traceability_ids.py +84 -0
- cloud_dog_api_kit/versioning/__init__.py +30 -0
- cloud_dog_api_kit/versioning/header.py +52 -0
- cloud_dog_api_kit/web/__init__.py +7 -0
- cloud_dog_api_kit/web/proxy.py +222 -0
- cloud_dog_api_kit/webhook/__init__.py +29 -0
- cloud_dog_api_kit/webhook/signature.py +149 -0
- cloud_dog_api_kit-0.13.0.dist-info/METADATA +27 -0
- cloud_dog_api_kit-0.13.0.dist-info/RECORD +98 -0
- cloud_dog_api_kit-0.13.0.dist-info/WHEEL +4 -0
- cloud_dog_api_kit-0.13.0.dist-info/licenses/LICENCE +190 -0
- cloud_dog_api_kit-0.13.0.dist-info/licenses/LICENSE +176 -0
- cloud_dog_api_kit-0.13.0.dist-info/licenses/NOTICE +7 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — A2A gateway helpers
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Helpers for mapping REST endpoints to A2A handlers.
|
|
20
|
+
# A2A endpoints MUST be thin wrappers over REST calls.
|
|
21
|
+
# Related requirements: FR15.1
|
|
22
|
+
# Related architecture: CC1.18
|
|
23
|
+
|
|
24
|
+
"""A2A gateway helpers for cloud_dog_api_kit."""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class A2AHandler:
|
|
34
|
+
"""A2A handler definition mapped from a REST endpoint.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
name: The handler name.
|
|
38
|
+
description: Human-readable description.
|
|
39
|
+
endpoint_path: The REST endpoint path this handler wraps.
|
|
40
|
+
method: HTTP method. Defaults to ``POST``.
|
|
41
|
+
input_schema: JSON Schema for the handler's input.
|
|
42
|
+
output_schema: JSON Schema for the handler's output.
|
|
43
|
+
|
|
44
|
+
Related tests: UT1.33_A2AGateway
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
description: str
|
|
49
|
+
endpoint_path: str
|
|
50
|
+
method: str = "POST"
|
|
51
|
+
input_schema: dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
output_schema: dict[str, Any] = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> dict[str, Any]:
|
|
55
|
+
"""Convert to a dictionary suitable for A2A registration.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
A dictionary with the handler definition.
|
|
59
|
+
"""
|
|
60
|
+
return {
|
|
61
|
+
"name": self.name,
|
|
62
|
+
"description": self.description,
|
|
63
|
+
"endpoint_path": self.endpoint_path,
|
|
64
|
+
"method": self.method,
|
|
65
|
+
"inputSchema": self.input_schema,
|
|
66
|
+
"outputSchema": self.output_schema,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def create_a2a_handler_from_endpoint(
|
|
71
|
+
endpoint_path: str,
|
|
72
|
+
method: str = "POST",
|
|
73
|
+
description: str = "",
|
|
74
|
+
name: str | None = None,
|
|
75
|
+
input_schema: dict[str, Any] | None = None,
|
|
76
|
+
output_schema: dict[str, Any] | None = None,
|
|
77
|
+
) -> A2AHandler:
|
|
78
|
+
"""Create an A2A handler from a REST endpoint.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
endpoint_path: The REST endpoint path.
|
|
82
|
+
method: HTTP method. Defaults to ``POST``.
|
|
83
|
+
description: Handler description.
|
|
84
|
+
name: Override handler name. Derived from path if None.
|
|
85
|
+
input_schema: JSON Schema for inputs.
|
|
86
|
+
output_schema: JSON Schema for outputs.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
An A2AHandler instance.
|
|
90
|
+
|
|
91
|
+
Related tests: UT1.33_A2AGateway
|
|
92
|
+
"""
|
|
93
|
+
if name is None:
|
|
94
|
+
parts = endpoint_path.strip("/").split("/")
|
|
95
|
+
filtered = [p for p in parts if not p.startswith("{") and p not in ("api", "v1", "v2")]
|
|
96
|
+
name = "_".join(filtered).replace(":", "_")
|
|
97
|
+
|
|
98
|
+
return A2AHandler(
|
|
99
|
+
name=name,
|
|
100
|
+
description=description,
|
|
101
|
+
endpoint_path=endpoint_path,
|
|
102
|
+
method=method,
|
|
103
|
+
input_schema=input_schema or {},
|
|
104
|
+
output_schema=output_schema or {},
|
|
105
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""A2A skill audit middleware for PS-50 compliance.
|
|
16
|
+
|
|
17
|
+
License: Apache 2.0
|
|
18
|
+
Ownership: Cloud-Dog, Viewdeck Engineering Limited
|
|
19
|
+
Description: Wraps A2A skill handlers with structured audit logging.
|
|
20
|
+
Requirements: PS-50.AUD2
|
|
21
|
+
Tasks: W28A-737
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
import time
|
|
28
|
+
import uuid
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from typing import Any, Callable, Optional
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def a2a_skill_audit_middleware(
|
|
34
|
+
skill_name: str,
|
|
35
|
+
handler: Callable,
|
|
36
|
+
*,
|
|
37
|
+
service: str,
|
|
38
|
+
logger: Optional[logging.Logger] = None,
|
|
39
|
+
) -> Callable:
|
|
40
|
+
"""Wrap an A2A skill handler to emit audit log entries for every invocation.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
skill_name: The name of the A2A skill being wrapped.
|
|
44
|
+
handler: The original skill handler callable.
|
|
45
|
+
service: The emitting service name (e.g. ``"file-mcp-server"``).
|
|
46
|
+
logger: Optional logger instance. Falls back to stdlib logging.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A wrapped handler that logs audit entries for each skill invocation.
|
|
50
|
+
|
|
51
|
+
Audit record fields:
|
|
52
|
+
- correlation_id
|
|
53
|
+
- service
|
|
54
|
+
- skill_name (which skill was invoked)
|
|
55
|
+
- actor (requesting agent or user)
|
|
56
|
+
- task_id (A2A task ID)
|
|
57
|
+
- outcome ("success" | "error")
|
|
58
|
+
- duration_ms
|
|
59
|
+
- timestamp
|
|
60
|
+
- error_detail (if outcome is "error")
|
|
61
|
+
"""
|
|
62
|
+
log = logger or logging.getLogger(f"cloud_dog_api_kit.a2a.audit.{service}")
|
|
63
|
+
|
|
64
|
+
def _wrapped(text: str, *, task_id: str = "", actor: str = "a2a-caller", **kwargs: Any) -> Any:
|
|
65
|
+
correlation_id = task_id or str(uuid.uuid4().hex[:16])
|
|
66
|
+
ts = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
67
|
+
t0 = time.monotonic()
|
|
68
|
+
try:
|
|
69
|
+
result = handler(text, **kwargs)
|
|
70
|
+
duration_ms = round((time.monotonic() - t0) * 1000, 2)
|
|
71
|
+
log.info(
|
|
72
|
+
"a2a_skill_invocation",
|
|
73
|
+
extra={
|
|
74
|
+
"event_type": "a2a_skill_invocation",
|
|
75
|
+
"correlation_id": correlation_id,
|
|
76
|
+
"service": service,
|
|
77
|
+
"skill_name": skill_name,
|
|
78
|
+
"actor": actor,
|
|
79
|
+
"task_id": task_id,
|
|
80
|
+
"outcome": "success",
|
|
81
|
+
"duration_ms": duration_ms,
|
|
82
|
+
"timestamp": ts,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
return result
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
duration_ms = round((time.monotonic() - t0) * 1000, 2)
|
|
88
|
+
log.warning(
|
|
89
|
+
"a2a_skill_invocation",
|
|
90
|
+
extra={
|
|
91
|
+
"event_type": "a2a_skill_invocation",
|
|
92
|
+
"correlation_id": correlation_id,
|
|
93
|
+
"service": service,
|
|
94
|
+
"skill_name": skill_name,
|
|
95
|
+
"actor": actor,
|
|
96
|
+
"task_id": task_id,
|
|
97
|
+
"outcome": "error",
|
|
98
|
+
"duration_ms": duration_ms,
|
|
99
|
+
"timestamp": ts,
|
|
100
|
+
"error_detail": str(exc),
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
raise
|
|
104
|
+
|
|
105
|
+
_wrapped.__name__ = handler.__name__ if hasattr(handler, "__name__") else skill_name
|
|
106
|
+
_wrapped.__doc__ = handler.__doc__
|
|
107
|
+
return _wrapped
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — Authentication and authorisation
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Auth dependency, RBAC helpers, and tenant isolation.
|
|
20
|
+
# Related requirements: FR3.1, FR3.2, FR3.3, FR3.4, FR3.5
|
|
21
|
+
# Related architecture: CC1.5, CC1.6
|
|
22
|
+
|
|
23
|
+
"""Authentication and authorisation for cloud_dog_api_kit."""
|
|
24
|
+
|
|
25
|
+
from cloud_dog_api_kit.auth.dependency import create_auth_dependency
|
|
26
|
+
from cloud_dog_api_kit.auth.rbac import require_admin, require_permission, require_tenant
|
|
27
|
+
from cloud_dog_api_kit.auth.service_auth import create_service_auth_dependency
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"create_auth_dependency",
|
|
31
|
+
"create_service_auth_dependency",
|
|
32
|
+
"require_admin",
|
|
33
|
+
"require_permission",
|
|
34
|
+
"require_tenant",
|
|
35
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — Authentication dependency for FastAPI
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: FastAPI dependency factory for API key / Bearer authentication.
|
|
20
|
+
# Supports both API key and Bearer token with configurable verify functions.
|
|
21
|
+
# Related requirements: FR3.4, FR3.5
|
|
22
|
+
# Related architecture: CC1.5
|
|
23
|
+
|
|
24
|
+
"""Authentication dependency factory for FastAPI."""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any, Callable
|
|
29
|
+
|
|
30
|
+
from fastapi import Request
|
|
31
|
+
|
|
32
|
+
from cloud_dog_api_kit.errors.exceptions import UnauthenticatedError
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_auth_dependency(
|
|
36
|
+
api_key_header: str = "X-API-Key",
|
|
37
|
+
bearer_verify_fn: Callable | None = None,
|
|
38
|
+
api_key_verify_fn: Callable | None = None,
|
|
39
|
+
config_api_key: str | None = None,
|
|
40
|
+
) -> Callable:
|
|
41
|
+
"""Create a FastAPI dependency for API key / Bearer authentication.
|
|
42
|
+
|
|
43
|
+
The dependency tries Bearer token first, then falls back to API key.
|
|
44
|
+
On success, sets ``request.state.user``, ``request.state.api_key``,
|
|
45
|
+
and ``request.state.tenant_id``.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
api_key_header: Header name for API key. Defaults to ``X-API-Key``.
|
|
49
|
+
bearer_verify_fn: Async/sync callable to verify Bearer tokens.
|
|
50
|
+
Should return a dict with ``user_id``, ``roles``, ``tenant_id``.
|
|
51
|
+
api_key_verify_fn: Async/sync callable to verify API keys.
|
|
52
|
+
Should return a dict with ``user_id``, ``roles``, ``tenant_id``.
|
|
53
|
+
config_api_key: Static API key from config for simple verification.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A FastAPI dependency callable.
|
|
57
|
+
|
|
58
|
+
Related tests: UT1.6_AuthDependency, UT1.7_BearerAuth, UT1.10_ServiceAuth
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
async def _auth_dependency(request: Request) -> dict[str, Any]:
|
|
62
|
+
# Try Bearer token first
|
|
63
|
+
auth_header = request.headers.get("authorization", "")
|
|
64
|
+
if auth_header.startswith("Bearer "):
|
|
65
|
+
token = auth_header[7:].strip()
|
|
66
|
+
if not token:
|
|
67
|
+
raise UnauthenticatedError(message="Empty Bearer token")
|
|
68
|
+
if bearer_verify_fn is not None:
|
|
69
|
+
import asyncio
|
|
70
|
+
|
|
71
|
+
if asyncio.iscoroutinefunction(bearer_verify_fn):
|
|
72
|
+
result = await bearer_verify_fn(token)
|
|
73
|
+
else:
|
|
74
|
+
result = bearer_verify_fn(token)
|
|
75
|
+
if result is None:
|
|
76
|
+
raise UnauthenticatedError(message="Invalid Bearer token")
|
|
77
|
+
_set_request_state(request, result)
|
|
78
|
+
return result
|
|
79
|
+
raise UnauthenticatedError(message="Bearer token verification not configured")
|
|
80
|
+
|
|
81
|
+
# Try API key
|
|
82
|
+
api_key = request.headers.get(api_key_header.lower(), "") or request.headers.get(api_key_header, "")
|
|
83
|
+
if not api_key:
|
|
84
|
+
raise UnauthenticatedError(message="Missing credentials")
|
|
85
|
+
|
|
86
|
+
if api_key_verify_fn is not None:
|
|
87
|
+
import asyncio
|
|
88
|
+
|
|
89
|
+
if asyncio.iscoroutinefunction(api_key_verify_fn):
|
|
90
|
+
result = await api_key_verify_fn(api_key)
|
|
91
|
+
else:
|
|
92
|
+
result = api_key_verify_fn(api_key)
|
|
93
|
+
if result is None:
|
|
94
|
+
raise UnauthenticatedError(message="Invalid API key")
|
|
95
|
+
_set_request_state(request, result)
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
# Fall back to config-based verification
|
|
99
|
+
if config_api_key is not None:
|
|
100
|
+
if api_key == config_api_key:
|
|
101
|
+
result = {"user_id": "api_key_user", "roles": [], "tenant_id": None}
|
|
102
|
+
_set_request_state(request, result)
|
|
103
|
+
return result
|
|
104
|
+
raise UnauthenticatedError(message="Invalid API key")
|
|
105
|
+
|
|
106
|
+
raise UnauthenticatedError(message="No authentication method configured")
|
|
107
|
+
|
|
108
|
+
return _auth_dependency
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _set_request_state(request: Request, auth_result: dict[str, Any]) -> None:
|
|
112
|
+
"""Set authentication result on request state.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
request: The FastAPI request.
|
|
116
|
+
auth_result: Dict with user_id, roles, tenant_id.
|
|
117
|
+
"""
|
|
118
|
+
request.state.user = auth_result.get("user_id")
|
|
119
|
+
request.state.api_key = True
|
|
120
|
+
request.state.tenant_id = auth_result.get("tenant_id")
|
|
121
|
+
request.state.roles = auth_result.get("roles", [])
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — RBAC helpers
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Role-based access control helpers for endpoint-level authorisation.
|
|
20
|
+
# Default-deny: all endpoints require auth unless explicitly marked public.
|
|
21
|
+
# Related requirements: FR3.3
|
|
22
|
+
# Related architecture: CC1.6
|
|
23
|
+
|
|
24
|
+
"""RBAC helpers for cloud_dog_api_kit."""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Callable
|
|
29
|
+
|
|
30
|
+
from fastapi import Request
|
|
31
|
+
|
|
32
|
+
from cloud_dog_api_kit.errors.exceptions import UnauthorisedError
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def require_permission(permission: str) -> Callable:
|
|
36
|
+
"""Create a FastAPI dependency that checks for a specific permission.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
permission: The required permission string.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A FastAPI dependency callable that raises UnauthorisedError if
|
|
43
|
+
the user does not have the required permission.
|
|
44
|
+
|
|
45
|
+
Related tests: UT1.8_RBACHelpers, SEC1.2_RBACEnforcement
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
async def _check(request: Request) -> None:
|
|
49
|
+
roles = getattr(request.state, "roles", []) or []
|
|
50
|
+
permissions = getattr(request.state, "permissions", []) or []
|
|
51
|
+
all_perms = set(roles) | set(permissions)
|
|
52
|
+
if permission not in all_perms:
|
|
53
|
+
raise UnauthorisedError(message=f"Missing required permission: {permission}")
|
|
54
|
+
|
|
55
|
+
return _check
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def require_admin() -> Callable:
|
|
59
|
+
"""Create a FastAPI dependency that checks for admin role.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A FastAPI dependency callable that raises UnauthorisedError if
|
|
63
|
+
the user does not have the admin role.
|
|
64
|
+
|
|
65
|
+
Related tests: UT1.8_RBACHelpers, SEC1.2_RBACEnforcement
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
async def _check(request: Request) -> None:
|
|
69
|
+
roles = getattr(request.state, "roles", []) or []
|
|
70
|
+
if "admin" not in roles:
|
|
71
|
+
raise UnauthorisedError(message="Admin access required")
|
|
72
|
+
|
|
73
|
+
return _check
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def require_tenant(tenant_id_param: str = "tenant_id") -> Callable:
|
|
77
|
+
"""Create a FastAPI dependency that enforces tenant isolation.
|
|
78
|
+
|
|
79
|
+
Checks that the authenticated user's tenant_id matches the requested
|
|
80
|
+
tenant_id from the path or query parameters.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
tenant_id_param: The name of the path/query parameter containing
|
|
84
|
+
the tenant ID. Defaults to ``tenant_id``.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A FastAPI dependency callable that raises UnauthorisedError on
|
|
88
|
+
tenant mismatch.
|
|
89
|
+
|
|
90
|
+
Related tests: UT1.9_TenantIsolation, SEC1.3_TenantIsolation
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
async def _check(request: Request) -> None:
|
|
94
|
+
user_tenant = getattr(request.state, "tenant_id", None)
|
|
95
|
+
if user_tenant is None:
|
|
96
|
+
return # No tenant context — skip check
|
|
97
|
+
|
|
98
|
+
# Check path params
|
|
99
|
+
requested_tenant = request.path_params.get(tenant_id_param)
|
|
100
|
+
if requested_tenant is None:
|
|
101
|
+
# Check query params
|
|
102
|
+
requested_tenant = request.query_params.get(tenant_id_param)
|
|
103
|
+
|
|
104
|
+
if requested_tenant is not None and requested_tenant != user_tenant:
|
|
105
|
+
raise UnauthorisedError(message="Access denied: tenant mismatch")
|
|
106
|
+
|
|
107
|
+
return _check
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — Service-to-service authentication
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Service-to-service auth using X-App-Id + service key.
|
|
20
|
+
# Related requirements: FR3.5
|
|
21
|
+
# Related architecture: SA1
|
|
22
|
+
|
|
23
|
+
"""Service-to-service authentication helpers."""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import Any, Callable
|
|
28
|
+
|
|
29
|
+
from fastapi import Header
|
|
30
|
+
|
|
31
|
+
from cloud_dog_api_kit.errors.exceptions import UnauthenticatedError
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_service_auth_dependency(
|
|
35
|
+
service_key_verify_fn: Callable[[str, str], dict[str, Any] | None],
|
|
36
|
+
app_id_header: str = "X-App-Id",
|
|
37
|
+
key_header: str = "X-Service-Key",
|
|
38
|
+
) -> Callable:
|
|
39
|
+
"""Create a dependency enforcing service-to-service authentication."""
|
|
40
|
+
|
|
41
|
+
async def _dep(
|
|
42
|
+
app_id: str | None = Header(default=None, alias=app_id_header),
|
|
43
|
+
service_key: str | None = Header(default=None, alias=key_header),
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
if not app_id or not service_key:
|
|
46
|
+
raise UnauthenticatedError(message="Missing service credentials")
|
|
47
|
+
|
|
48
|
+
result = service_key_verify_fn(app_id, service_key)
|
|
49
|
+
if result is None:
|
|
50
|
+
raise UnauthenticatedError(message="Invalid service credentials")
|
|
51
|
+
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
return _dep
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — HTTP client helpers
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Service-to-service HTTP client creation and retry utilities.
|
|
20
|
+
# Related requirements: FR9.1, FR9.2
|
|
21
|
+
# Related architecture: SA1
|
|
22
|
+
|
|
23
|
+
"""HTTP client helpers for cloud_dog_api_kit."""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from cloud_dog_api_kit.clients.http_client import ClientTimeout, RetryPolicy, create_http_client
|
|
28
|
+
|
|
29
|
+
__all__ = ["ClientTimeout", "RetryPolicy", "create_http_client"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# cloud_dog_api_kit — Circuit breaker hook (optional)
|
|
16
|
+
#
|
|
17
|
+
# Licence: Proprietary — Cloud-Dog AI Platform
|
|
18
|
+
# Owner: Cloud-Dog AI
|
|
19
|
+
# Description: Optional circuit breaker hooks for HTTP client integrations.
|
|
20
|
+
# Related requirements: FR9.1
|
|
21
|
+
# Related architecture: SA1
|
|
22
|
+
|
|
23
|
+
"""Circuit breaker hooks for cloud_dog_api_kit."""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import Protocol
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CircuitBreaker(Protocol):
|
|
31
|
+
"""Optional circuit breaker protocol."""
|
|
32
|
+
|
|
33
|
+
def on_success(self) -> None:
|
|
34
|
+
"""Record a successful protected call."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
def on_failure(self) -> None:
|
|
38
|
+
"""Record a failed protected call."""
|
|
39
|
+
...
|