Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,16 @@ campus client new --name "My App" --description "My application"
# Generate the client's secret (only time it is shown)
campus client revoke --client-id <id>

# Manage users (requires users:* scopes and users-admin designation)
# Manage users (needs a users:* grant row AND the matching scope)
campus user list
campus user new --email "new.member@school.edu" --name "New Member"
campus user activate --user-id "new.member@school.edu"

# See — and administer — who can do what (#883 grant store)
campus grant list
campus grant grant --grantee-type user --grantee-id someone@school.edu \
--resource-type users --level write

# List vault entries
campus vault list --vault myvault

Expand Down Expand Up @@ -78,23 +83,33 @@ The semantics are exact-request, not Google-style accumulation:
`users:write` ⊇ `users:mod` ⊇ `users:read`, so request only the
highest tier you need. The users tiers map to commands: read
lists/gets, mod activates, write creates/renames, admin deletes.
- **Management authority = grant row AND scope.** Carrying a
management scope is only half the story: the authorizing account
must also hold a matching row in the access-grant store —
`campus grant list` shows the matrix, `campus grant grant/revoke`
administers it (operator or a same-vocabulary admin only; no
principal administers its own grants, and vocabulary-level
`clients:admin` rows are rejected). Clients grant rows cap at
write: registering or deleting clients stays operator/root-only
(campus invariant A8, nyjc-computing/campus#883).
- **The super-admin root is the one exception** (A8 carve-out): the
env-nominated `AUTH_SUPER_ADMIN` account holds full authority with
no grant rows and no scope requirement. Safeguard it; use it to
seed grants, then work day-to-day on a sub-admin account.
- **The client allowlist is fail-closed.** A scope outside the
CLI client's registered `allowed_scopes` fails the login
outright with `invalid_scope`; an operator must widen the
allowlist first (`campus client update --client-id <id>
--allowed-scope <scope>`). Carrying a management scope is still
not enough on its own — the authorizing account must also be a
designated admin user server-side (campus invariant A8): clients
scopes consult `AUTH_ADMIN_USER_IDS`, users scopes consult
`AUTH_USERS_ADMIN_USER_IDS` (a clients-admin is not a
users-admin).
--allowed-scope <scope>`).

**Check before you re-login:** `campus auth status` reports the
scopes the stored token carries (along with the client, endpoints,
principal and expiry), so scripts and agents can answer "what can
this token do?" before hitting a 403. Use `--json` for
machine-readable output; `--offline` skips the validity check when
no network is wanted. Scopes are recorded locally at login —
no network is wanted. It also shows your grant rows when your
account may read them (operator, root, or grants admin). Scopes are
recorded locally at login —
credentials stored before this was recorded show them as unknown
until the next login. `campus auth status` exits 1 when you are not
logged in or the server rejects the stored token, and 0 otherwise
Expand Down
5 changes: 5 additions & 0 deletions campus_cli/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ def auth_users(self):
"""Access the auth users resource."""
return self.campus.auth.users

@property
def auth_grants(self):
"""Access the auth access-grant store resource."""
return self.campus.auth.grants

@property
def api_timetables(self):
"""Access the API timetable resource."""
Expand Down
3 changes: 2 additions & 1 deletion campus_cli/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Authentication commands for Campus CLI."""

from campus_cli.auth.client import client_app
from campus_cli.auth.grants import grants_app
from campus_cli.auth.login import login_app
from campus_cli.auth.user import user_app
from campus_cli.auth.vault import vault_app

__all__ = ["client_app", "login_app", "user_app", "vault_app"]
__all__ = ["client_app", "grants_app", "login_app", "user_app", "vault_app"]
13 changes: 13 additions & 0 deletions campus_cli/auth/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ def client_new(
By default the client is confidential: the client secret is NOT
returned; use 'campus client revoke' to generate and retrieve it.
Public clients (--public) have no client secret at all.

Authority: operator or super-admin root — client registration is
not grantable through the access-grant store (campus#883).
"""
if dry_run:
new_kwargs = [
Expand Down Expand Up @@ -361,6 +364,11 @@ def client_update(
stored value: the API layer full-replaces each provided field, so
re-pass every entry the client must keep. Fields you omit are left
untouched.

Authority: your own client's profile fields are self-service;
other clients need clients:write authority (grant row plus scope);
the scope caps and token-bridge flag stay operator/root-only
(campus#883 — clients grant rows cap at write).
"""
if clear_allowed_scopes and allowed_scope:
print_error(
Expand Down Expand Up @@ -455,6 +463,8 @@ def client_delete(
Delete an OAuth client.

Permanently deletes the specified OAuth client.

Authority: operator or super-admin root (campus#883).
"""
if dry_run:
print_python_api(
Expand Down Expand Up @@ -490,6 +500,9 @@ def client_revoke(
Revokes the current client secret and generates a new one.
The new secret will be displayed (this is the only way to retrieve it).
Use this when first creating a client or when you need to rotate the secret.

Authority: the client itself (own secret), or the operator /
super-admin root (campus#883).
"""
if dry_run:
print_python_api(
Expand Down
Loading
Loading