auths-python 0.1.0__cp38-abi3-win_amd64.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.
- auths/__init__.py +147 -0
- auths/__init__.pyi +486 -0
- auths/_client.py +713 -0
- auths/_errors.py +80 -0
- auths/_native.pyd +0 -0
- auths/agent.py +55 -0
- auths/artifact.py +60 -0
- auths/attestation_query.py +141 -0
- auths/audit.py +226 -0
- auths/commit.py +28 -0
- auths/devices.py +162 -0
- auths/doctor.py +109 -0
- auths/git.py +473 -0
- auths/identity.py +221 -0
- auths/jwt.py +253 -0
- auths/org.py +310 -0
- auths/pairing.py +216 -0
- auths/policy.py +382 -0
- auths/py.typed +0 -0
- auths/rotation.py +30 -0
- auths/sign.py +5 -0
- auths/trust.py +169 -0
- auths/verify.py +111 -0
- auths/witness.py +91 -0
- auths_python-0.1.0.dist-info/METADATA +152 -0
- auths_python-0.1.0.dist-info/RECORD +28 -0
- auths_python-0.1.0.dist-info/WHEEL +4 -0
- auths_python-0.1.0.dist-info/sboms/auths-python.cyclonedx.json +14418 -0
auths/jwt.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""JWT validation for Auths OIDC tokens.
|
|
2
|
+
|
|
3
|
+
Requires PyJWT: pip install auths-python[jwt]
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("auths.jwt")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AuthsClaims:
|
|
16
|
+
"""Validated claims from an Auths OIDC token."""
|
|
17
|
+
|
|
18
|
+
sub: str
|
|
19
|
+
"""Subject claim — the signer's DID."""
|
|
20
|
+
keri_prefix: str
|
|
21
|
+
"""KERI prefix of the identity."""
|
|
22
|
+
capabilities: list[str]
|
|
23
|
+
"""Capabilities granted by this token."""
|
|
24
|
+
iss: str
|
|
25
|
+
"""Issuer claim — the OIDC bridge URL."""
|
|
26
|
+
aud: str
|
|
27
|
+
"""Audience claim — the service this token is intended for."""
|
|
28
|
+
exp: int
|
|
29
|
+
"""Expiration time as Unix timestamp."""
|
|
30
|
+
iat: int
|
|
31
|
+
"""Issued-at time as Unix timestamp."""
|
|
32
|
+
jti: str
|
|
33
|
+
"""Unique JWT ID for replay prevention."""
|
|
34
|
+
signer_type: str | None = None
|
|
35
|
+
"""Signer classification: `"Human"`, `"Agent"`, or `"Workload"`."""
|
|
36
|
+
delegated_by: str | None = None
|
|
37
|
+
"""DID of the delegating identity, if this is a delegated token."""
|
|
38
|
+
witness_quorum: dict | None = None
|
|
39
|
+
"""Witness quorum metadata, if witness-backed."""
|
|
40
|
+
github_actor: str | None = None
|
|
41
|
+
"""GitHub username, present for GitHub Actions OIDC tokens."""
|
|
42
|
+
github_repository: str | None = None
|
|
43
|
+
"""GitHub repository (owner/repo), present for GitHub Actions OIDC tokens."""
|
|
44
|
+
|
|
45
|
+
def has_capability(self, cap: str) -> bool:
|
|
46
|
+
"""Check if token grants a specific capability."""
|
|
47
|
+
return cap in self.capabilities
|
|
48
|
+
|
|
49
|
+
def has_any_capability(self, caps: list[str]) -> bool:
|
|
50
|
+
"""Check if token grants any of the listed capabilities."""
|
|
51
|
+
return any(c in self.capabilities for c in caps)
|
|
52
|
+
|
|
53
|
+
def has_all_capabilities(self, caps: list[str]) -> bool:
|
|
54
|
+
"""Check if token grants all of the listed capabilities."""
|
|
55
|
+
return all(c in self.capabilities for c in caps)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_agent(self) -> bool:
|
|
59
|
+
return self.signer_type == "Agent"
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def is_human(self) -> bool:
|
|
63
|
+
return self.signer_type == "Human"
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def is_delegated(self) -> bool:
|
|
67
|
+
return self.delegated_by is not None
|
|
68
|
+
|
|
69
|
+
def __repr__(self) -> str:
|
|
70
|
+
caps = ", ".join(self.capabilities[:3])
|
|
71
|
+
if len(self.capabilities) > 3:
|
|
72
|
+
caps += f" +{len(self.capabilities) - 3}"
|
|
73
|
+
sub_short = self.sub[:25] if len(self.sub) > 25 else self.sub
|
|
74
|
+
return (
|
|
75
|
+
f"AuthsClaims(sub='{sub_short}...', "
|
|
76
|
+
f"caps=[{caps}], type={self.signer_type})"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AuthsJWKSClient:
|
|
81
|
+
"""JWKS client with automatic key caching for Auths OIDC token validation.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
jwks_url: The OIDC bridge's JWKS endpoint.
|
|
85
|
+
cache_ttl: How long to cache JWKS keys, in seconds (default: 300).
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
```python
|
|
89
|
+
jwks = AuthsJWKSClient("https://bridge.example.com/.well-known/jwks.json")
|
|
90
|
+
claims = jwks.verify_token(token, audience="my-service")
|
|
91
|
+
```
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, jwks_url: str, *, cache_ttl: int = 300):
|
|
95
|
+
try:
|
|
96
|
+
import jwt as pyjwt
|
|
97
|
+
from jwt import PyJWKClient
|
|
98
|
+
except ImportError:
|
|
99
|
+
raise ImportError(
|
|
100
|
+
"PyJWT is required for JWT validation. "
|
|
101
|
+
"Install it with: pip install auths-python[jwt]"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
from auths._errors import NetworkError
|
|
105
|
+
|
|
106
|
+
self._pyjwt = pyjwt
|
|
107
|
+
self._NetworkError = NetworkError
|
|
108
|
+
self._client = PyJWKClient(jwks_url, cache_jwk_set=True, lifespan=cache_ttl)
|
|
109
|
+
self._jwks_url = jwks_url
|
|
110
|
+
logger.debug("Initialized JWKS client for %s (cache_ttl=%ds)", jwks_url, cache_ttl)
|
|
111
|
+
|
|
112
|
+
def verify_token(
|
|
113
|
+
self,
|
|
114
|
+
token: str,
|
|
115
|
+
*,
|
|
116
|
+
audience: str,
|
|
117
|
+
issuer: str | None = None,
|
|
118
|
+
leeway: int = 60,
|
|
119
|
+
) -> AuthsClaims:
|
|
120
|
+
"""Verify an Auths OIDC token and extract claims.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
token: Raw JWT bearer token string.
|
|
124
|
+
audience: Expected audience claim.
|
|
125
|
+
issuer: Expected issuer claim (optional, verified if set).
|
|
126
|
+
leeway: Clock skew tolerance in seconds (default: 60).
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
AuthsClaims with the validated token claims.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
VerificationError: If the token is expired, has wrong audience/issuer, or invalid signature.
|
|
133
|
+
NetworkError: If JWKS keys cannot be fetched.
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
```python
|
|
137
|
+
claims = jwks.verify_token(bearer_token, audience="my-service")
|
|
138
|
+
if claims.has_capability("read"):
|
|
139
|
+
allow_access()
|
|
140
|
+
```
|
|
141
|
+
"""
|
|
142
|
+
from auths._errors import VerificationError
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
signing_key = self._client.get_signing_key_from_jwt(token)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.warning("JWKS fetch failed for %s: %s", self._jwks_url, e)
|
|
148
|
+
raise self._NetworkError(
|
|
149
|
+
f"Failed to fetch JWKS from {self._jwks_url}: {e}",
|
|
150
|
+
code="jwks_fetch_failed",
|
|
151
|
+
should_retry=True,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
options = {"verify_aud": True, "verify_exp": True}
|
|
156
|
+
if issuer:
|
|
157
|
+
options["verify_iss"] = True
|
|
158
|
+
|
|
159
|
+
decoded = self._pyjwt.decode(
|
|
160
|
+
token,
|
|
161
|
+
signing_key.key,
|
|
162
|
+
algorithms=["RS256", "ES256", "EdDSA"],
|
|
163
|
+
audience=audience,
|
|
164
|
+
issuer=issuer,
|
|
165
|
+
leeway=leeway,
|
|
166
|
+
options=options,
|
|
167
|
+
)
|
|
168
|
+
except self._pyjwt.ExpiredSignatureError:
|
|
169
|
+
raise VerificationError(
|
|
170
|
+
"Token expired. Request a new token from the OIDC bridge.",
|
|
171
|
+
code="token_expired",
|
|
172
|
+
)
|
|
173
|
+
except self._pyjwt.InvalidAudienceError:
|
|
174
|
+
raise VerificationError(
|
|
175
|
+
f"Token audience does not match expected '{audience}'. "
|
|
176
|
+
"Ensure the token was issued for this service.",
|
|
177
|
+
code="invalid_audience",
|
|
178
|
+
)
|
|
179
|
+
except self._pyjwt.InvalidIssuerError:
|
|
180
|
+
raise VerificationError(
|
|
181
|
+
f"Token issuer does not match expected '{issuer}'. "
|
|
182
|
+
"Check the OIDC bridge URL configuration.",
|
|
183
|
+
code="invalid_issuer",
|
|
184
|
+
)
|
|
185
|
+
except self._pyjwt.InvalidSignatureError:
|
|
186
|
+
raise VerificationError(
|
|
187
|
+
"Token signature is invalid. The token may have been tampered with "
|
|
188
|
+
"or the JWKS keys may have rotated. Try refreshing the JWKS cache.",
|
|
189
|
+
code="invalid_signature",
|
|
190
|
+
)
|
|
191
|
+
except self._pyjwt.DecodeError as e:
|
|
192
|
+
raise VerificationError(
|
|
193
|
+
f"Token decode failed: {e}. Ensure the token is a valid JWT string.",
|
|
194
|
+
code="decode_error",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
logger.debug(
|
|
198
|
+
"Token verified: sub=%s, caps=%s",
|
|
199
|
+
decoded.get("sub"),
|
|
200
|
+
decoded.get("capabilities"),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return AuthsClaims(
|
|
204
|
+
sub=decoded["sub"],
|
|
205
|
+
keri_prefix=decoded.get("keri_prefix", ""),
|
|
206
|
+
capabilities=decoded.get("capabilities", []),
|
|
207
|
+
iss=decoded.get("iss", ""),
|
|
208
|
+
aud=decoded.get("aud", ""),
|
|
209
|
+
exp=decoded.get("exp", 0),
|
|
210
|
+
iat=decoded.get("iat", 0),
|
|
211
|
+
jti=decoded.get("jti", ""),
|
|
212
|
+
signer_type=decoded.get("signer_type"),
|
|
213
|
+
delegated_by=decoded.get("delegated_by"),
|
|
214
|
+
witness_quorum=decoded.get("witness_quorum"),
|
|
215
|
+
github_actor=decoded.get("github_actor"),
|
|
216
|
+
github_repository=decoded.get("github_repository"),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def verify_token(
|
|
221
|
+
token: str,
|
|
222
|
+
*,
|
|
223
|
+
jwks_url: str,
|
|
224
|
+
audience: str,
|
|
225
|
+
issuer: str | None = None,
|
|
226
|
+
leeway: int = 60,
|
|
227
|
+
) -> AuthsClaims:
|
|
228
|
+
"""Verify an Auths OIDC token (one-shot, no JWKS caching).
|
|
229
|
+
|
|
230
|
+
For production use, prefer `AuthsJWKSClient` which caches JWKS keys.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
token: Raw JWT bearer token string.
|
|
234
|
+
jwks_url: URL to fetch JSON Web Key Set.
|
|
235
|
+
audience: Expected audience claim.
|
|
236
|
+
issuer: Expected issuer claim (optional).
|
|
237
|
+
leeway: Clock skew tolerance in seconds (default: 60).
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
AuthsClaims with the validated token claims.
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
VerificationError: If the token is invalid.
|
|
244
|
+
NetworkError: If JWKS keys cannot be fetched.
|
|
245
|
+
|
|
246
|
+
Examples:
|
|
247
|
+
```python
|
|
248
|
+
from auths.jwt import verify_token
|
|
249
|
+
claims = verify_token(token, jwks_url="...", audience="my-service")
|
|
250
|
+
```
|
|
251
|
+
"""
|
|
252
|
+
client = AuthsJWKSClient(jwks_url)
|
|
253
|
+
return client.verify_token(token, audience=audience, issuer=issuer, leeway=leeway)
|
auths/org.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from auths._native import (
|
|
8
|
+
add_org_member as _add_org_member,
|
|
9
|
+
create_org as _create_org,
|
|
10
|
+
list_org_members as _list_org_members,
|
|
11
|
+
revoke_org_member as _revoke_org_member,
|
|
12
|
+
)
|
|
13
|
+
from auths._client import _map_error
|
|
14
|
+
from auths._errors import OrgError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Org:
|
|
19
|
+
"""An organization identity."""
|
|
20
|
+
|
|
21
|
+
prefix: str
|
|
22
|
+
"""KERI prefix of the organization identity."""
|
|
23
|
+
did: str
|
|
24
|
+
"""The organization's DID (`did:keri:...`)."""
|
|
25
|
+
label: str
|
|
26
|
+
"""Human-readable organization name."""
|
|
27
|
+
repo_path: str
|
|
28
|
+
"""Path to the identity repository."""
|
|
29
|
+
|
|
30
|
+
def __repr__(self):
|
|
31
|
+
return f"Org(did={self.did!r}, label={self.label!r})"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class OrgMember:
|
|
36
|
+
"""A member within an organization."""
|
|
37
|
+
|
|
38
|
+
member_did: str
|
|
39
|
+
"""DID of the member."""
|
|
40
|
+
role: str
|
|
41
|
+
"""Member role: `"admin"`, `"member"`, or `"readonly"`."""
|
|
42
|
+
capabilities: list[str]
|
|
43
|
+
"""Capabilities granted to this member."""
|
|
44
|
+
issuer_did: str
|
|
45
|
+
"""DID of the identity that issued the membership attestation."""
|
|
46
|
+
attestation_rid: str
|
|
47
|
+
"""RID of the membership attestation."""
|
|
48
|
+
revoked: bool
|
|
49
|
+
"""Whether this membership has been revoked."""
|
|
50
|
+
expires_at: Optional[str]
|
|
51
|
+
"""ISO 8601 expiry timestamp, or None for non-expiring memberships."""
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_admin(self) -> bool:
|
|
55
|
+
"""Whether this member has admin role."""
|
|
56
|
+
return self.role == "admin"
|
|
57
|
+
|
|
58
|
+
def __repr__(self):
|
|
59
|
+
status = " revoked" if self.revoked else ""
|
|
60
|
+
return f"OrgMember(did={self.member_did!r}, role={self.role!r}{status})"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OrgService:
|
|
64
|
+
"""Resource service for organization operations."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, client):
|
|
67
|
+
self._client = client
|
|
68
|
+
|
|
69
|
+
def create(
|
|
70
|
+
self,
|
|
71
|
+
label: str,
|
|
72
|
+
repo_path: str | None = None,
|
|
73
|
+
passphrase: str | None = None,
|
|
74
|
+
) -> Org:
|
|
75
|
+
"""Create a new organization identity.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
label: Human-readable name for the org.
|
|
79
|
+
repo_path: Override identity store path.
|
|
80
|
+
passphrase: Override passphrase.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Org with the KERI prefix, DID, and label.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
OrgError: If organization creation fails.
|
|
87
|
+
KeychainError: If the keychain is locked or inaccessible.
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
```python
|
|
91
|
+
org = client.orgs.create("my-team")
|
|
92
|
+
```
|
|
93
|
+
"""
|
|
94
|
+
rp = repo_path or self._client.repo_path
|
|
95
|
+
pp = passphrase or self._client._passphrase
|
|
96
|
+
try:
|
|
97
|
+
prefix, did, lbl, rpath = _create_org(label, rp, pp)
|
|
98
|
+
return Org(prefix=prefix, did=did, label=lbl, repo_path=rpath)
|
|
99
|
+
except (ValueError, RuntimeError) as exc:
|
|
100
|
+
raise _map_error(exc, default_cls=OrgError) from exc
|
|
101
|
+
|
|
102
|
+
def add_member(
|
|
103
|
+
self,
|
|
104
|
+
org_did: str,
|
|
105
|
+
member_did: str,
|
|
106
|
+
role: str = "member",
|
|
107
|
+
capabilities: list[str] | None = None,
|
|
108
|
+
note: str | None = None,
|
|
109
|
+
repo_path: str | None = None,
|
|
110
|
+
passphrase: str | None = None,
|
|
111
|
+
member_public_key_hex: str | None = None,
|
|
112
|
+
) -> OrgMember:
|
|
113
|
+
"""Add a member to an organization.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
org_did: The organization's DID (`did:keri:...`).
|
|
117
|
+
member_did: The member's DID to add.
|
|
118
|
+
role: One of `"admin"`, `"member"`, `"readonly"`.
|
|
119
|
+
capabilities: Explicit capability list. If None, uses role defaults.
|
|
120
|
+
note: Optional human-readable note for the attestation.
|
|
121
|
+
member_public_key_hex: Member's Ed25519 public key hex. Required when
|
|
122
|
+
the member's identity is in a different registry.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
OrgMember with the membership attestation details.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
OrgError: If the member cannot be added.
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
```python
|
|
132
|
+
member = client.orgs.add_member(org.did, dev.did, role="member")
|
|
133
|
+
```
|
|
134
|
+
"""
|
|
135
|
+
rp = repo_path or self._client.repo_path
|
|
136
|
+
pp = passphrase or self._client._passphrase
|
|
137
|
+
caps_json = json.dumps(capabilities) if capabilities else None
|
|
138
|
+
try:
|
|
139
|
+
m_did, r, caps_str, issuer, rid, revoked, expires = _add_org_member(
|
|
140
|
+
org_did, member_did, role, rp, caps_json, pp, note,
|
|
141
|
+
member_public_key_hex,
|
|
142
|
+
)
|
|
143
|
+
return OrgMember(
|
|
144
|
+
member_did=m_did,
|
|
145
|
+
role=r,
|
|
146
|
+
capabilities=json.loads(caps_str) if caps_str else [],
|
|
147
|
+
issuer_did=issuer,
|
|
148
|
+
attestation_rid=rid,
|
|
149
|
+
revoked=revoked,
|
|
150
|
+
expires_at=expires,
|
|
151
|
+
)
|
|
152
|
+
except (ValueError, RuntimeError) as exc:
|
|
153
|
+
raise _map_error(exc, default_cls=OrgError) from exc
|
|
154
|
+
|
|
155
|
+
def revoke_member(
|
|
156
|
+
self,
|
|
157
|
+
org_did: str,
|
|
158
|
+
member_did: str,
|
|
159
|
+
note: str | None = None,
|
|
160
|
+
repo_path: str | None = None,
|
|
161
|
+
passphrase: str | None = None,
|
|
162
|
+
member_public_key_hex: str | None = None,
|
|
163
|
+
) -> OrgMember:
|
|
164
|
+
"""Revoke a member's authorization.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
org_did: The organization's DID.
|
|
168
|
+
member_did: The member's DID to revoke.
|
|
169
|
+
note: Optional human-readable note.
|
|
170
|
+
member_public_key_hex: Member's Ed25519 public key hex. Required when
|
|
171
|
+
the member's identity is in a different registry.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
OrgMember with revoked status.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
OrgError: If the member cannot be revoked.
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
```python
|
|
181
|
+
revoked = client.orgs.revoke_member(org.did, dev.did)
|
|
182
|
+
```
|
|
183
|
+
"""
|
|
184
|
+
rp = repo_path or self._client.repo_path
|
|
185
|
+
pp = passphrase or self._client._passphrase
|
|
186
|
+
try:
|
|
187
|
+
m_did, r, caps_str, issuer, rid, revoked, expires = _revoke_org_member(
|
|
188
|
+
org_did, member_did, rp, pp, note, member_public_key_hex,
|
|
189
|
+
)
|
|
190
|
+
return OrgMember(
|
|
191
|
+
member_did=m_did,
|
|
192
|
+
role=r,
|
|
193
|
+
capabilities=json.loads(caps_str) if caps_str else [],
|
|
194
|
+
issuer_did=issuer,
|
|
195
|
+
attestation_rid=rid,
|
|
196
|
+
revoked=revoked,
|
|
197
|
+
expires_at=expires,
|
|
198
|
+
)
|
|
199
|
+
except (ValueError, RuntimeError) as exc:
|
|
200
|
+
raise _map_error(exc, default_cls=OrgError) from exc
|
|
201
|
+
|
|
202
|
+
def update_member(
|
|
203
|
+
self,
|
|
204
|
+
org_did: str,
|
|
205
|
+
member_did: str,
|
|
206
|
+
role: str | None = None,
|
|
207
|
+
capabilities: list[str] | None = None,
|
|
208
|
+
note: str | None = None,
|
|
209
|
+
repo_path: str | None = None,
|
|
210
|
+
passphrase: str | None = None,
|
|
211
|
+
member_public_key_hex: str | None = None,
|
|
212
|
+
) -> OrgMember:
|
|
213
|
+
"""Update a member's role or capabilities.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
org_did: The organization's DID.
|
|
217
|
+
member_did: The member's DID to update.
|
|
218
|
+
role: New role. If None, keeps current.
|
|
219
|
+
capabilities: New capabilities. If None, uses role defaults.
|
|
220
|
+
note: Optional note.
|
|
221
|
+
member_public_key_hex: Member's Ed25519 public key hex. Required when
|
|
222
|
+
the member's identity is in a different registry.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
OrgMember with the updated role and capabilities.
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
OrgError: If the member cannot be updated.
|
|
229
|
+
|
|
230
|
+
Examples:
|
|
231
|
+
```python
|
|
232
|
+
updated = client.orgs.update_member(org.did, dev.did, role="admin")
|
|
233
|
+
```
|
|
234
|
+
"""
|
|
235
|
+
self.revoke_member(
|
|
236
|
+
org_did, member_did, note="superseded by update",
|
|
237
|
+
repo_path=repo_path, passphrase=passphrase,
|
|
238
|
+
member_public_key_hex=member_public_key_hex,
|
|
239
|
+
)
|
|
240
|
+
return self.add_member(
|
|
241
|
+
org_did, member_did, role=role or "member",
|
|
242
|
+
capabilities=capabilities, note=note,
|
|
243
|
+
repo_path=repo_path, passphrase=passphrase,
|
|
244
|
+
member_public_key_hex=member_public_key_hex,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def list_members(
|
|
248
|
+
self,
|
|
249
|
+
org_did: str,
|
|
250
|
+
include_revoked: bool = False,
|
|
251
|
+
repo_path: str | None = None,
|
|
252
|
+
) -> list[OrgMember]:
|
|
253
|
+
"""List all members of an organization.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
org_did: The organization's DID.
|
|
257
|
+
include_revoked: If True, includes revoked members.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of OrgMember objects.
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
OrgError: If the organization doesn't exist.
|
|
264
|
+
|
|
265
|
+
Examples:
|
|
266
|
+
```python
|
|
267
|
+
members = client.orgs.list_members(org.did)
|
|
268
|
+
```
|
|
269
|
+
"""
|
|
270
|
+
rp = repo_path or self._client.repo_path
|
|
271
|
+
try:
|
|
272
|
+
members_json = _list_org_members(org_did, include_revoked, rp)
|
|
273
|
+
raw = json.loads(members_json)
|
|
274
|
+
return [
|
|
275
|
+
OrgMember(
|
|
276
|
+
member_did=m["member_did"],
|
|
277
|
+
role=m["role"],
|
|
278
|
+
capabilities=m["capabilities"],
|
|
279
|
+
issuer_did=m["issuer_did"],
|
|
280
|
+
attestation_rid=m["attestation_rid"],
|
|
281
|
+
revoked=m["revoked"],
|
|
282
|
+
expires_at=m.get("expires_at"),
|
|
283
|
+
)
|
|
284
|
+
for m in raw
|
|
285
|
+
]
|
|
286
|
+
except (ValueError, RuntimeError) as exc:
|
|
287
|
+
raise _map_error(exc, default_cls=OrgError) from exc
|
|
288
|
+
|
|
289
|
+
def get_member(
|
|
290
|
+
self,
|
|
291
|
+
org_did: str,
|
|
292
|
+
member_did: str,
|
|
293
|
+
repo_path: str | None = None,
|
|
294
|
+
) -> OrgMember | None:
|
|
295
|
+
"""Look up a specific member.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
org_did: The organization's DID.
|
|
299
|
+
member_did: The member's DID to look up.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
OrgMember if found, or None.
|
|
303
|
+
|
|
304
|
+
Examples:
|
|
305
|
+
```python
|
|
306
|
+
member = client.orgs.get_member(org.did, dev.did)
|
|
307
|
+
```
|
|
308
|
+
"""
|
|
309
|
+
members = self.list_members(org_did, include_revoked=False, repo_path=repo_path)
|
|
310
|
+
return next((m for m in members if m.member_did == member_did), None)
|