mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-29 10:26:21 +02:00
iam: self-service ops, optional workspace filters, Mux service routing (#855)
Three threads, all reinforcing the contract's system-level vs.
workspace-association distinction.
WS Mux service routing
- tg-show-flows (and any workspace-level service over the WS) was
failing with "unknown service" because the post-refactor Mux
unconditionally looked up flow-service:<kind>. Now branches on
the envelope's flow field: with flow → flow-service:<kind>;
without flow → <kind>:<op> from the inner body; with bare op
lookup for service=iam. Resource and parameters come from the
matched op's own extractors — same path the HTTP endpoints take.
Optional workspace on system-level user/key ops
- list-users returns the deployment-wide list when no workspace is
supplied, filters when one is. get-user, update-user,
disable-user, enable-user, delete-user, reset-password,
create-api-key, list-api-keys, revoke-api-key all treat workspace
as an optional integrity check rather than a required argument.
- create-user keeps workspace required — there it's the new user's
home-workspace binding, a parameter rather than an address.
- API keys reclassified as SYSTEM-level resources. By the same
reasoning that makes users system-level, an API key is a
credential record on a deployment-wide registry; the workspace it
authenticates to is a property, not a containment.
Self-service surface
- whoami: returns the caller's own user record. AUTHENTICATED-only;
no users:read capability required. Foundation for UI affordances
that depend on the caller's permissions.
- bootstrap-status: POST /api/v1/auth/bootstrap-status, PUBLIC,
side-effect-free. Returns {bootstrap_available: bool} so a
first-run UI can decide whether to render setup without consuming
the bootstrap op.
- Gateway now injects actor=identity.handle on every authenticated
forward to iam-svc (IamEndpoint and WS Mux iam path), overwriting
any caller-supplied value. Underpins whoami, audit logging, and
future regime-side decisions that need actor identity.
- tg-whoami and tg-update-user CLIs.
Spec polish
- iam-contract.md: actor-injection rule documented; whoami /
bootstrap-status added to operations list; permission-scope
framing tightened (workspace scope is a property of the grant,
not the user or role).
- iam.md: self-service section; gateway flow gains the actor-
injection step; role section reframed so iam-svc constraints
don't leak into contract-level prose.
- iam-protocol.md: ops table updated for whoami, bootstrap-status,
optional-workspace pattern; bootstrap_available added to the
IamResponse listing.
This commit is contained in:
parent
6302eb8c97
commit
9fc1d4527b
15 changed files with 555 additions and 147 deletions
|
|
@ -44,6 +44,8 @@ tg-bootstrap-iam = "trustgraph.cli.bootstrap_iam:main"
|
|||
tg-login = "trustgraph.cli.login:main"
|
||||
tg-create-user = "trustgraph.cli.create_user:main"
|
||||
tg-list-users = "trustgraph.cli.list_users:main"
|
||||
tg-whoami = "trustgraph.cli.whoami:main"
|
||||
tg-update-user = "trustgraph.cli.update_user:main"
|
||||
tg-disable-user = "trustgraph.cli.disable_user:main"
|
||||
tg-enable-user = "trustgraph.cli.enable_user:main"
|
||||
tg-delete-user = "trustgraph.cli.delete_user:main"
|
||||
|
|
|
|||
125
trustgraph-cli/trustgraph/cli/update_user.py
Normal file
125
trustgraph-cli/trustgraph/cli/update_user.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
"""
|
||||
Update a user's profile fields: name, email, roles, enabled flag,
|
||||
must-change-password flag.
|
||||
|
||||
Username is immutable — create a new user and disable the old one
|
||||
to effect a username change. Password changes go through
|
||||
``tg-change-password`` (self-service) or ``tg-reset-password``
|
||||
(admin-driven).
|
||||
|
||||
Only the fields you supply are changed; omitted fields are left
|
||||
untouched on the user record. An empty ``--roles`` is rejected by
|
||||
iam-svc (a user must have at least one role); to demote a user use
|
||||
``tg-disable-user``.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from ._iam import DEFAULT_URL, DEFAULT_TOKEN, call_iam, run_main
|
||||
|
||||
|
||||
def _parse_bool(s):
|
||||
if s is None:
|
||||
return None
|
||||
s = s.strip().lower()
|
||||
if s in ("yes", "y", "true", "t", "1"):
|
||||
return True
|
||||
if s in ("no", "n", "false", "f", "0"):
|
||||
return False
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"expected yes/no, got {s!r}"
|
||||
)
|
||||
|
||||
|
||||
def do_update_user(args):
|
||||
user = {}
|
||||
if args.name is not None:
|
||||
user["name"] = args.name
|
||||
if args.email is not None:
|
||||
user["email"] = args.email
|
||||
if args.roles is not None:
|
||||
user["roles"] = args.roles
|
||||
if args.enabled is not None:
|
||||
user["enabled"] = args.enabled
|
||||
if args.must_change_password is not None:
|
||||
user["must_change_password"] = args.must_change_password
|
||||
|
||||
if not user:
|
||||
print(
|
||||
"tg-update-user: nothing to change — supply at least "
|
||||
"one of --name / --email / --roles / --enabled / "
|
||||
"--must-change-password",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
req = {
|
||||
"operation": "update-user",
|
||||
"user_id": args.user_id,
|
||||
"user": user,
|
||||
}
|
||||
if args.workspace:
|
||||
req["workspace"] = args.workspace
|
||||
resp = call_iam(args.api_url, args.token, req)
|
||||
|
||||
rec = resp.get("user", {})
|
||||
print(f"id : {rec.get('id', '')}")
|
||||
print(f"username : {rec.get('username', '')}")
|
||||
print(f"name : {rec.get('name', '')}")
|
||||
print(f"email : {rec.get('email', '')}")
|
||||
print(f"workspace : {rec.get('workspace', '')}")
|
||||
print(f"roles : {', '.join(rec.get('roles', []))}")
|
||||
print(f"enabled : {'yes' if rec.get('enabled') else 'no'}")
|
||||
print(
|
||||
f"must-change-pw: "
|
||||
f"{'yes' if rec.get('must_change_password') else 'no'}"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="tg-update-user", description=__doc__,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-u", "--api-url", default=DEFAULT_URL,
|
||||
help=f"API URL (default: {DEFAULT_URL})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t", "--token", default=DEFAULT_TOKEN,
|
||||
help="Auth token (default: $TRUSTGRAPH_TOKEN)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--user-id", required=True, help="Target user id",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--name", default=None, help="New display name",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--email", default=None, help="New email",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--roles", nargs="+", default=None,
|
||||
help="Replacement role list (e.g. --roles reader writer)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enabled", type=_parse_bool, default=None,
|
||||
help="Set enabled flag (yes/no)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--must-change-password", type=_parse_bool, default=None,
|
||||
help="Set must-change-password flag (yes/no)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-w", "--workspace", default=None,
|
||||
help=(
|
||||
"Optional workspace integrity check — when supplied, "
|
||||
"iam-svc verifies the target user's home workspace "
|
||||
"matches"
|
||||
),
|
||||
)
|
||||
run_main(do_update_user, parser)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
52
trustgraph-cli/trustgraph/cli/whoami.py
Normal file
52
trustgraph-cli/trustgraph/cli/whoami.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""
|
||||
Show the authenticated caller's own user record.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
import tabulate
|
||||
|
||||
from ._iam import DEFAULT_URL, DEFAULT_TOKEN, call_iam, run_main
|
||||
|
||||
|
||||
def do_whoami(args):
|
||||
resp = call_iam(args.api_url, args.token, {"operation": "whoami"})
|
||||
user = resp.get("user")
|
||||
if not user:
|
||||
print("(no user record returned)")
|
||||
return
|
||||
|
||||
rows = [
|
||||
["id", user.get("id", "")],
|
||||
["username", user.get("username", "")],
|
||||
["name", user.get("name", "")],
|
||||
["email", user.get("email", "")],
|
||||
["workspace", user.get("workspace", "")],
|
||||
["roles", ", ".join(user.get("roles", []))],
|
||||
["enabled", "yes" if user.get("enabled") else "no"],
|
||||
[
|
||||
"must change password",
|
||||
"yes" if user.get("must_change_password") else "no",
|
||||
],
|
||||
["created", user.get("created", "")],
|
||||
]
|
||||
print(tabulate.tabulate(rows, tablefmt="plain"))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="tg-whoami", description=__doc__,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-u", "--api-url", default=DEFAULT_URL,
|
||||
help=f"API URL (default: {DEFAULT_URL})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t", "--token", default=DEFAULT_TOKEN,
|
||||
help="Auth token (default: $TRUSTGRAPH_TOKEN)",
|
||||
)
|
||||
run_main(do_whoami, parser)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue