Skip to content

docs: make required OAuth scopes discoverable before a 403, not after - #109

Open
jackvaughanjr wants to merge 1 commit into
okta:mainfrom
jackvaughanjr:fix/scope-discoverability
Open

docs: make required OAuth scopes discoverable before a 403, not after#109
jackvaughanjr wants to merge 1 commit into
okta:mainfrom
jackvaughanjr:fix/scope-discoverability

Conversation

@jackvaughanjr

Copy link
Copy Markdown

Independent of #107 and #108; no shared code beyond three lines in server.py.

Relates to #104.

Three separate gaps made the scope model hard to reason about until it failed at runtime.

1. The scope-to-tool table had drifted

get_login_failures was absent from the README entirely, despite being registered in TOOL_SCOPE_REGISTRY. confirm_delete_group and confirm_delete_application were missing from their rows. The table was regenerated from the registry and diffed programmatically rather than checked by eye.

2. The README promised a scope hierarchy the code does not implement

It stated that a token with *.manage implicitly covers *.read. scope_guard has always matched exact strings:

if required_scope not in configured:

So declaring only okta.users.manage silently removes list_users, get_user, and get_user_profile_attributes from tools/list. They do not 403; they are simply absent, and the operator has no way to find out why.

Two things could be fixed here: the doc, or the code. This PR fixes the doc and keeps exact matching, because OKTA_SCOPES is more useful as an exact, auditable declaration of intent than as something a reader has to expand mentally. Okta's API genuinely does grant read via .manage, so the README now names that difference explicitly rather than leaving it as a surprise.

What changes in the code is that the consequence is no longer silent. prune_tools_by_scope now emits a warning naming the missing .read scope and the exact tools it cost:

WARNING [scope-guard] Configured scope 'okta.users.manage' does NOT implicitly enable
'okta.users.read' for this server. The following tool(s) remain disabled because
'okta.users.read' is missing from OKTA_SCOPES: ['get_user', 'get_user_profile_attributes',
'list_users']. Remediation: add 'okta.users.read' to OKTA_SCOPES.

get_scope_status reports the same under a new additive manage_without_read_gaps key, so the model can tell a user what to add. The sibling scope is derived from the string, not a hardcoded resource list, and okta.logs.read (which has no .manage counterpart) produces no spurious warning.

The same false claim in scope_registry.py's own module docstring is corrected.

3. Configured and granted scopes were conflated

OKTA_SCOPES declares what you want. The Okta Admin Console controls what you are granted. Startup pruning only sees the former, so a scope declared but never granted passes preflight and fails at call time with an HTTP 403 instead of being hidden. Now documented with a table of the three states, a troubleshooting entry keyed on the literal error text so it is greppable by someone who just hit it, and a note that a missing admin-role assignment is a second possible cause of a 403.

Also adds copy-pasteable read-only and full OKTA_SCOPES profiles, generated from the registry rather than typed by hand. The read-only profile is the minimum set for a compliance audit.

Testing

10 tests: manage-only triggers the warning naming the scope and the three tools, both-scopes and read-only produce no warning, okta.logs.read alone neither warns nor crashes, get_scope_status reports the condition and stays clean when absent, and module state resets between calls. Assertions are on captured loguru records, since this codebase uses loguru directly and caplog does not intercept it.

One test asserts the README no longer contains the false claim, as a cheap guard against the doc drifting back.

No behavior changes for a correctly configured server. Full suite: 539 passed, up from 529 on main.

Three gaps made the scope model hard to reason about before running
into it at runtime.

1. The scope-to-tool table had drifted from TOOL_SCOPE_REGISTRY.
   get_login_failures was absent from the README entirely, and
   confirm_delete_group and confirm_delete_application were missing from
   their rows. The table is now verified against the registry.

2. The README claimed a token with '*.manage' implicitly covers '*.read'.
   The code has never done that: scope_guard matches exact strings, so
   declaring only okta.users.manage silently removes list_users,
   get_user, and get_user_profile_attributes from tools/list. They do not
   error, they are simply absent. The docs now describe the real
   behavior, and prune_tools_by_scope logs a warning naming the missing
   '.read' scope and the tools it cost. get_scope_status reports the same
   under manage_without_read_gaps. The same false claim in
   scope_registry.py's own docstring is corrected.

   Exact matching is kept deliberately: OKTA_SCOPES stays an auditable
   declaration of intent rather than something a reader has to expand
   mentally.

3. OKTA_SCOPES declares what you want; the Okta Admin Console controls
   what you are granted. Startup pruning only sees the former, so a scope
   declared but not granted produces a runtime 403 instead of a hidden
   tool. Documented, with a troubleshooting entry keyed on the literal
   error text and ready-made read-only and full scope profiles.

Relates to okta#104.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant