agentauthlayer 0.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.
- agent_auth/__init__.py +48 -0
- agent_auth/__main__.py +4 -0
- agent_auth/agents.py +40 -0
- agent_auth/audit.py +51 -0
- agent_auth/auth.py +36 -0
- agent_auth/cli.py +28 -0
- agent_auth/client.py +107 -0
- agent_auth/context.py +21 -0
- agent_auth/core.py +638 -0
- agent_auth/delegation.py +15 -0
- agent_auth/exceptions.py +72 -0
- agent_auth/models.py +72 -0
- agent_auth/policy.py +296 -0
- agent_auth/policy_service.py +176 -0
- agent_auth/principals.py +44 -0
- agent_auth/registry.py +90 -0
- agent_auth/session.py +135 -0
- agent_auth/storage.py +536 -0
- agent_auth/tokens.py +92 -0
- agent_auth/users.py +173 -0
- agentauthlayer-0.1.0.dist-info/METADATA +131 -0
- agentauthlayer-0.1.0.dist-info/RECORD +24 -0
- agentauthlayer-0.1.0.dist-info/WHEEL +5 -0
- agentauthlayer-0.1.0.dist-info/top_level.txt +1 -0
agent_auth/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""agent_auth — reusable authentication and authorization primitives for Agent Auth."""
|
|
2
|
+
|
|
3
|
+
from agent_auth.auth import principal_fields, principal_from_agent, principal_from_user
|
|
4
|
+
from agent_auth.client import AuthAPIClient
|
|
5
|
+
from agent_auth.context import AuthContext
|
|
6
|
+
from agent_auth.delegation import DelegationGrant
|
|
7
|
+
from agent_auth.exceptions import (
|
|
8
|
+
AgentAuthError,
|
|
9
|
+
AgentNotFoundError,
|
|
10
|
+
AuthServiceError,
|
|
11
|
+
InvalidTokenError,
|
|
12
|
+
PermissionDeniedError,
|
|
13
|
+
RevokedTokenError,
|
|
14
|
+
)
|
|
15
|
+
from agent_auth.models import Agent, TokenClaims, TokenRecord, User, UserClaims
|
|
16
|
+
from agent_auth.policy import PolicyDecision, PolicyEvaluator, PolicyRequest, PolicyStatement, RoleDefinition, require_permission
|
|
17
|
+
from agent_auth.principals import AgentPrincipal, Principal, SystemPrincipal, UserPrincipal, user_scopes_for_role
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Agent",
|
|
21
|
+
"AgentAuthError",
|
|
22
|
+
"AgentNotFoundError",
|
|
23
|
+
"AgentPrincipal",
|
|
24
|
+
"AuthAPIClient",
|
|
25
|
+
"AuthContext",
|
|
26
|
+
"AuthServiceError",
|
|
27
|
+
"DelegationGrant",
|
|
28
|
+
"InvalidTokenError",
|
|
29
|
+
"PermissionDeniedError",
|
|
30
|
+
"PolicyDecision",
|
|
31
|
+
"PolicyEvaluator",
|
|
32
|
+
"PolicyRequest",
|
|
33
|
+
"PolicyStatement",
|
|
34
|
+
"Principal",
|
|
35
|
+
"RevokedTokenError",
|
|
36
|
+
"RoleDefinition",
|
|
37
|
+
"SystemPrincipal",
|
|
38
|
+
"TokenClaims",
|
|
39
|
+
"TokenRecord",
|
|
40
|
+
"User",
|
|
41
|
+
"UserClaims",
|
|
42
|
+
"UserPrincipal",
|
|
43
|
+
"principal_fields",
|
|
44
|
+
"principal_from_agent",
|
|
45
|
+
"principal_from_user",
|
|
46
|
+
"require_permission",
|
|
47
|
+
"user_scopes_for_role",
|
|
48
|
+
]
|
agent_auth/__main__.py
ADDED
agent_auth/agents.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol, TypeVar
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
AgentT = TypeVar("AgentT")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AgentStore(Protocol[AgentT]):
|
|
10
|
+
def save(self, agent: AgentT) -> AgentT:
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
def get(self, agent_id: str) -> AgentT | None:
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
def list_all(self, project_id: str | None = None) -> list[AgentT]:
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentIdentityService:
|
|
21
|
+
"""Core agent lifecycle orchestration over a configured agent store."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, store: AgentStore[AgentT]):
|
|
24
|
+
self.store = store
|
|
25
|
+
|
|
26
|
+
def save(self, agent: AgentT) -> AgentT:
|
|
27
|
+
return self.store.save(agent)
|
|
28
|
+
|
|
29
|
+
def get(self, agent_id: str) -> AgentT | None:
|
|
30
|
+
return self.store.get(agent_id)
|
|
31
|
+
|
|
32
|
+
def list_agents(self, project_id: str | None = None) -> list[AgentT]:
|
|
33
|
+
return self.store.list_all(project_id=project_id)
|
|
34
|
+
|
|
35
|
+
def update_scopes(self, agent_id: str, scopes: list[str]) -> AgentT | None:
|
|
36
|
+
agent = self.store.get(agent_id)
|
|
37
|
+
if not agent:
|
|
38
|
+
return None
|
|
39
|
+
agent.scopes = scopes
|
|
40
|
+
return self.store.save(agent)
|
agent_auth/audit.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""agent_auth audit logger — pluggable, default prints to stderr."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import Any, Protocol
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("agent_auth.audit")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuditBackend(Protocol):
|
|
15
|
+
"""Interface that any custom audit backend must implement."""
|
|
16
|
+
|
|
17
|
+
def log(self, event: dict[str, Any]) -> None: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StderrAuditBackend:
|
|
21
|
+
"""Default backend — writes JSON lines to stderr."""
|
|
22
|
+
|
|
23
|
+
def log(self, event: dict[str, Any]) -> None:
|
|
24
|
+
line = json.dumps(event, default=str)
|
|
25
|
+
print(line, file=sys.stderr)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InMemoryAuditBackend:
|
|
29
|
+
"""Keeps events in a list — useful for testing."""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self.events: list[dict[str, Any]] = []
|
|
33
|
+
|
|
34
|
+
def log(self, event: dict[str, Any]) -> None:
|
|
35
|
+
self.events.append(event)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AuditLogger:
|
|
39
|
+
"""Thin wrapper around a backend that stamps every event."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, backend: AuditBackend | None = None) -> None:
|
|
42
|
+
self._backend: AuditBackend = backend or StderrAuditBackend()
|
|
43
|
+
|
|
44
|
+
def record(self, event_type: str, **details: Any) -> None:
|
|
45
|
+
event = {
|
|
46
|
+
"event": event_type,
|
|
47
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
48
|
+
**details,
|
|
49
|
+
}
|
|
50
|
+
self._backend.log(event)
|
|
51
|
+
logger.debug("audit: %s", event)
|
agent_auth/auth.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from agent_auth.principals import AgentPrincipal, Principal, UserPrincipal, user_scopes_for_role
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def principal_from_agent(*, agent_id: str, role: str | None = None, scopes: list[str] | None = None, owner: str | None = None, status: str | None = None) -> AgentPrincipal:
|
|
7
|
+
return AgentPrincipal(
|
|
8
|
+
principal_id=agent_id,
|
|
9
|
+
principal_type='agent',
|
|
10
|
+
role=role,
|
|
11
|
+
scopes=list(scopes or []),
|
|
12
|
+
owner=owner,
|
|
13
|
+
status=status,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def principal_from_user(*, user_id: str, email: str, role: str, scopes: list[str] | None = None, status: str | None = None) -> UserPrincipal:
|
|
18
|
+
effective_scopes = list(scopes) if scopes is not None else user_scopes_for_role(role)
|
|
19
|
+
return UserPrincipal(
|
|
20
|
+
principal_id=user_id,
|
|
21
|
+
principal_type='user',
|
|
22
|
+
role=role,
|
|
23
|
+
scopes=effective_scopes,
|
|
24
|
+
email=email,
|
|
25
|
+
status=status,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def principal_fields(principal: Principal) -> tuple[str, str, list[str], str | None, str | None]:
|
|
30
|
+
return (
|
|
31
|
+
principal.principal_id,
|
|
32
|
+
principal.principal_type,
|
|
33
|
+
list(principal.scopes),
|
|
34
|
+
principal.role,
|
|
35
|
+
principal.email,
|
|
36
|
+
)
|
agent_auth/cli.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
import uvicorn
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
parser = argparse.ArgumentParser(
|
|
7
|
+
description="Agent Auth Platform CLI - Manage and run the control plane.",
|
|
8
|
+
prog="agent-auth"
|
|
9
|
+
)
|
|
10
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
11
|
+
|
|
12
|
+
# The "ui" command (like "mlflow ui")
|
|
13
|
+
ui_parser = subparsers.add_parser("ui", help="Launch the integrated Auth API and React Dashboard")
|
|
14
|
+
ui_parser.add_argument("--host", default="0.0.0.0", help="Host to bind to (default: 0.0.0.0)")
|
|
15
|
+
ui_parser.add_argument("--port", type=int, default=8002, help="Port to bind to (default: 8002)")
|
|
16
|
+
ui_parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
|
|
17
|
+
|
|
18
|
+
args = parser.parse_args()
|
|
19
|
+
|
|
20
|
+
if args.command == "ui":
|
|
21
|
+
print(f"Starting Agent Auth UI & Control Plane on http://{args.host}:{args.port}")
|
|
22
|
+
uvicorn.run("auth_app.main:app", host=args.host, port=args.port, reload=args.reload)
|
|
23
|
+
else:
|
|
24
|
+
parser.print_help()
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|
agent_auth/client.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from agent_auth.context import AuthContext
|
|
9
|
+
from agent_auth.exceptions import (
|
|
10
|
+
AgentNotFoundError,
|
|
11
|
+
AuthServiceError,
|
|
12
|
+
InvalidTokenError,
|
|
13
|
+
PermissionDeniedError,
|
|
14
|
+
RevokedTokenError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthAPIClient:
|
|
19
|
+
"""Thin SDK client for talking to the Agent Auth control plane."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, base_url: str | None = None, token: str | None = None, timeout: int = 30) -> None:
|
|
22
|
+
self.base_url = (base_url or os.getenv("AGENT_AUTH_URL") or "http://127.0.0.1:8002").rstrip("/")
|
|
23
|
+
self.token = token or os.getenv("AGENT_AUTH_TOKEN")
|
|
24
|
+
self.timeout = timeout
|
|
25
|
+
|
|
26
|
+
def _headers(self) -> dict[str, str]:
|
|
27
|
+
headers = {"Content-Type": "application/json"}
|
|
28
|
+
if self.token:
|
|
29
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
30
|
+
return headers
|
|
31
|
+
|
|
32
|
+
def _request(self, method: str, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
33
|
+
response = requests.request(
|
|
34
|
+
method,
|
|
35
|
+
f"{self.base_url}{path}",
|
|
36
|
+
json=json,
|
|
37
|
+
headers=self._headers(),
|
|
38
|
+
timeout=self.timeout,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if response.ok:
|
|
42
|
+
if not response.content:
|
|
43
|
+
return None
|
|
44
|
+
return response.json()
|
|
45
|
+
|
|
46
|
+
detail = None
|
|
47
|
+
try:
|
|
48
|
+
detail = response.json().get("detail")
|
|
49
|
+
except Exception:
|
|
50
|
+
detail = response.text or "Request failed"
|
|
51
|
+
|
|
52
|
+
if response.status_code == 401:
|
|
53
|
+
if detail and ("revoked" in detail.lower() or "inactive" in detail.lower()):
|
|
54
|
+
raise RevokedTokenError(detail)
|
|
55
|
+
raise InvalidTokenError(detail or "Invalid token")
|
|
56
|
+
if response.status_code == 403:
|
|
57
|
+
raise PermissionDeniedError(detail or "Permission denied")
|
|
58
|
+
if response.status_code == 404 and path.startswith("/agents/"):
|
|
59
|
+
raise AgentNotFoundError(detail or "Agent not found")
|
|
60
|
+
raise AuthServiceError(detail or f"Request failed with status {response.status_code}")
|
|
61
|
+
|
|
62
|
+
def health(self) -> dict[str, Any]:
|
|
63
|
+
return self._request("GET", "/health")
|
|
64
|
+
|
|
65
|
+
def create_agent(self, agent_id: str, name: str, owner: str, role: str, scopes: list[str], project_id: str | None = None) -> dict[str, Any]:
|
|
66
|
+
payload = {
|
|
67
|
+
"agent_id": agent_id,
|
|
68
|
+
"name": name,
|
|
69
|
+
"owner": owner,
|
|
70
|
+
"role": role,
|
|
71
|
+
"scopes": scopes,
|
|
72
|
+
}
|
|
73
|
+
if project_id:
|
|
74
|
+
payload["project_id"] = project_id
|
|
75
|
+
return self._request("POST", "/agents", json=payload)
|
|
76
|
+
|
|
77
|
+
def get_agent(self, agent_id: str) -> dict[str, Any]:
|
|
78
|
+
return self._request("GET", f"/agents/{agent_id}")
|
|
79
|
+
|
|
80
|
+
def issue_token(self, agent_id: str) -> dict[str, Any]:
|
|
81
|
+
return self._request("POST", "/auth/token", json={"agent_id": agent_id})
|
|
82
|
+
|
|
83
|
+
def sync_tools(self, permissions: list[dict[str, str]]) -> dict[str, Any]:
|
|
84
|
+
return self._request("POST", "/policy/permissions/sync", json={"permissions": permissions})
|
|
85
|
+
|
|
86
|
+
def evaluate(self, principal_id: str, action: str, resource: str | None = None, granted_scopes: list[str] | None = None, role: str | None = None, context: dict[str, str] | None = None) -> dict[str, Any]:
|
|
87
|
+
return self._request(
|
|
88
|
+
"POST",
|
|
89
|
+
"/policy/evaluate",
|
|
90
|
+
json={
|
|
91
|
+
"principal_id": principal_id,
|
|
92
|
+
"action": action,
|
|
93
|
+
"resource": resource,
|
|
94
|
+
"granted_scopes": granted_scopes or [],
|
|
95
|
+
"role": role,
|
|
96
|
+
"context": context or {},
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def auth_context(self, principal_id: str, scopes: list[str], role: str | None = None, principal_type: str = "agent", email: str | None = None) -> AuthContext:
|
|
101
|
+
return AuthContext(
|
|
102
|
+
principal_id=principal_id,
|
|
103
|
+
principal_type=principal_type,
|
|
104
|
+
scopes=scopes,
|
|
105
|
+
role=role,
|
|
106
|
+
email=email,
|
|
107
|
+
)
|
agent_auth/context.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(slots=True)
|
|
7
|
+
class AuthContext:
|
|
8
|
+
principal_id: str
|
|
9
|
+
principal_type: str
|
|
10
|
+
jti: str
|
|
11
|
+
scopes: list[str] = field(default_factory=list)
|
|
12
|
+
role: str | None = None
|
|
13
|
+
email: str | None = None
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def subject_id(self) -> str:
|
|
17
|
+
return self.principal_id
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def subject_type(self) -> str:
|
|
21
|
+
return self.principal_type
|