captured admin.plugin-enable

Enable and disable an AuthMethod plugin in a realm

An admin of the realm asks the admin API to enable an installed plugin. The pipeline checks the permission plugins.security, which only a token with qauth:admin:security uses, then the approval the request names; the handler checks that the plugin is installed, that the state is the one the request was written against, and that the realm's config of the plugin passes the plugin's own schema, then turns it on. While it is on, the plugin's routes under /upstream/<type> answer on the realm host; off, they answer the not_found document of a path no route serves. Disabling takes the same checks but the config one, and keeps the config.

What this view shows

one admin enabling, then disabling, one installed plugin in one realm, and the plugin's routes in between.

Open world: what is not drawn here is not ruled out. Only the guarantees below are binding.

Left out of this view:

The main story

When: Its role holds plugins.security and its token carries qauth:admin:security, QAuth-Approval names an approval filed for exactly this request by this caller, approved and unexpired, the path names a plugin this deployment loaded at boot, the body's version, and If-Match when present, name the version of the plugin state the handler reads, the realm's config of the plugin, its secrets decrypted, passes the plugin's config schema.

  1. The plugin is off in the realm: an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/enable
  2. Check the permission and the approval of the enable: the caller holds the permission and the approval, which the handler transaction consumes
  3. Check the plugin, its state and its config, and turn it on: the row is written enabled, conditional on its version, and the audit row records the change
  4. The plugin is on in the realm: a browser requests one of the plugin's routes on the realm host

Result: Hand the request to the plugin's route.

The flow

an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/enable a browser requests one of the plugin's routes under /upstream/<type> on the realm host the role lacks the permission: no token would pass, and the challenge names no scope only the token withholds the permission: qauth:admin:write is refused no approval for exactly this request: the operation never runs the caller holds the permission and the approval, which the handler transaction consumes the path names no installed plugin the state moved since the caller read it, or its approver saw it: the body names the version the approval binds the realm's config does not pass the plugin's schema: a plugin is on only with a config it accepts the row is written enabled, conditional on its version, and the audit row records the change a browser requests one of the plugin's routes on the realm host an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/disable the role lacks the permission only the token withholds the permission no approval for exactly this request the caller holds the permission and the approval the path names no installed plugin the state moved since the caller read it the row is written off, its config kept, conditional on its version a browser requests one of the plugin's routes on the realm host start Every installed plugin starts off in every realm: a realm with no row for it has it off. Its config, if the realm set one, is kept. The plugin is off in the realm pluginOff · server step plugins.enable is a security operation with the effect credential-path: it needs plugins.security, a permission of class security, which a token uses only with qauth:admin:security. A security operation runs only with the approval its request names. Check the permission and the approval of the enable authorizingEnable · server step enabling Check the plugin, its state and its config, and turn it on enabling · server step pluginOn The plugin is on in the realm pluginOn · server step plugins.disable changes which upstream credentials sign users in too: a security operation with credential-path and the same permission, so a token with qauth:admin:write cannot switch a sign-in method off either. Check the permission and the approval of the disable authorizingDisable · server step disabling Check the plugin and its state, and turn it off disabling · server step pluginOffAgain The plugin is off again, its config kept pluginOffAgain · server step Before the plugin is enabled and once it is disabled again, the route's preParsing hook answers before the body is read, after the rate limit and without its headers, with the not_found document an unknown path gets: neither its status, its body nor a validation error tells whether the realm has the plugin on. Past the rate limit it answers 429 where an unknown path answers 404, which tells a plugin of that type is installed, never which realms have it on. Answer the plugin's route as a path no route serves ends here The route mount runs: the host checks the callback's binding against the pending transaction before the plugin runs (#599). Until the sign-in flow offers the upstream step (#639) no transaction is pending, and the callback is answered 400. Hand the request to the plugin's route ends here forbidden.no_permission Refuse a caller whose role lacks the permission ends here insufficient_scope.no_security_scope Refuse a token without qauth:admin:security, naming the scope ends here approval_required.no_approval Refuse a security operation without its approval ends here not_found.plugin_unknown Refuse a plugin the deployment did not load ends here precondition_failed.state_moved Refuse a request written against another state ends here The problem names each failing member and the schema's code, never a value. Refuse to enable a plugin whose config its schema refuses ends here
an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/enable a browser requests one of the plugin's routes under /upstream/<type> on the realm host the role lacks the permission: no token would pass, and the challenge names no scope only the token withholds the permission: qauth:admin:write is refused no approval for exactly this request: the operation never runs the caller holds the permission and the approval, which the handler transaction consumes the path names no installed plugin the state moved since the caller read it, or its approver saw it: the body names the version the approval binds the realm's config does not pass the plugin's schema: a plugin is on only with a config it accepts the row is written enabled, conditional on its version, and the audit row records the change a browser requests one of the plugin's routes on the realm host an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/disable the role lacks the permission only the token withholds the permission no approval for exactly this request the caller holds the permission and the approval the path names no installed plugin the state moved since the caller read it the row is written off, its config kept, conditional on its version a browser requests one of the plugin's routes on the realm host start Every installed plugin starts off in every realm: a realm with no row for it has it off. Its config, if the realm set one, is kept. The plugin is off in the realm pluginOff · server step plugins.enable is a security operation with the effect credential-path: it needs plugins.security, a permission of class security, which a token uses only with qauth:admin:security. A security operation runs only with the approval its request names. Check the permission and the approval of the enable authorizingEnable · server step enabling Check the plugin, its state and its config, and turn it on enabling · server step pluginOn The plugin is on in the realm pluginOn · server step plugins.disable changes which upstream credentials sign users in too: a security operation with credential-path and the same permission, so a token with qauth:admin:write cannot switch a sign-in method off either. Check the permission and the approval of the disable authorizingDisable · server step disabling Check the plugin and its state, and turn it off disabling · server step pluginOffAgain The plugin is off again, its config kept pluginOffAgain · server step Before the plugin is enabled and once it is disabled again, the route's preParsing hook answers before the body is read, after the rate limit and without its headers, with the not_found document an unknown path gets: neither its status, its body nor a validation error tells whether the realm has the plugin on. Past the rate limit it answers 429 where an unknown path answers 404, which tells a plugin of that type is installed, never which realms have it on. Answer the plugin's route as a path no route serves ends here The route mount runs: the host checks the callback's binding against the pending transaction before the plugin runs (#599). Until the sign-in flow offers the upstream step (#639) no transaction is pending, and the callback is answered 400. Hand the request to the plugin's route ends here forbidden.no_permission Refuse a caller whose role lacks the permission ends here insufficient_scope.no_security_scope Refuse a token without qauth:admin:security, naming the scope ends here approval_required.no_approval Refuse a security operation without its approval ends here not_found.plugin_unknown Refuse a plugin the deployment did not load ends here precondition_failed.state_moved Refuse a request written against another state ends here The problem names each failing member and the schema's code, never a value. Refuse to enable a plugin whose config its schema refuses ends here

blue line: the main storydotted line: decided by a rule, nobody actsdashed grey box: the server decides, nothing is shown

What each step answers

StepThe outside world sees
Answer the plugin's route as a path no route serves
not_found.plugin_off
404 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#not-found","status":404,"code":"not_found"}
Hand the request to the plugin's route
bad_request.no_pending_sign_in
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#bad-request","status":400,"code":"bad_request"}
Refuse a caller whose role lacks the permission
forbidden.no_permission
403 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#forbidden","status":403,"code":"forbidden"}
Refuse a token without qauth:admin:security, naming the scope
insufficient_scope.no_security_scope
403 · headers: {"WWW-Authenticate":"Bearer realm=\"OAuth\", error=\"insufficient_scope\", scope=\"qauth:admin:security\""} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#forbidden","status":403,"code":"forbidden"}
Refuse a security operation without its approval
approval_required.no_approval
403 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#approval-required","status":403,"code":"approval_required"}
Refuse a plugin the deployment did not load
not_found.plugin_unknown
404 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#not-found","status":404,"code":"not_found"}
Refuse a request written against another state
precondition_failed.state_moved
412 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#precondition-failed","status":412,"code":"precondition_failed"}
Refuse to enable a plugin whose config its schema refuses
bad_request.config_invalid
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#bad-request","status":400,"code":"bad_request"}

How it decides what happens next

Check the permission and the approval of the enable

#IfThen
1the role lacks the permission: no token would pass, and the challenge names no scopeRefuse a caller whose role lacks the permission
2only the token withholds the permission: qauth:admin:write is refusedRefuse a token without qauth:admin:security, naming the scope
3no approval for exactly this request: the operation never runsRefuse a security operation without its approval
4the caller holds the permission and the approval, which the handler transaction consumesCheck the plugin, its state and its config, and turn it on

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1roleLacksPermissionforbidden.no_permission16 of 48
2scopeLacksSecurityinsufficient_scope.no_security_scope16 of 48
3noApprovalapproval_required.no_approval8 of 48
4otherwiseenabling8 of 48
roleLacksPermission
caller == 'noSecurityRole'
scopeLacksSecurity
caller == 'writeScopeOnly'
noApproval
approval == 'missing'

Check the plugin, its state and its config, and turn it on

#IfThen
1the path names no installed pluginRefuse a plugin the deployment did not load
2the state moved since the caller read it, or its approver saw it: the body names the version the approval bindsRefuse a request written against another state
3the realm's config does not pass the plugin's schema: a plugin is on only with a config it acceptsRefuse to enable a plugin whose config its schema refuses
4the row is written enabled, conditional on its version, and the audit row records the changeThe plugin is on in the realm

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1pluginUnknownnot_found.plugin_unknown4 of 8
2stateMovedprecondition_failed.state_moved2 of 8
3configInvalidbad_request.config_invalid1 of 8
4otherwisepluginOn1 of 8
pluginUnknown
plugin == 'unknown'
stateMoved
stateMatch == 'moved'
configInvalid
config == 'invalid'

Check the permission and the approval of the disable

#IfThen
1the role lacks the permissionRefuse a caller whose role lacks the permission
2only the token withholds the permissionRefuse a token without qauth:admin:security, naming the scope
3no approval for exactly this requestRefuse a security operation without its approval
4the caller holds the permission and the approvalCheck the plugin and its state, and turn it off

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1roleLacksPermissionforbidden.no_permission0 of 1
2scopeLacksSecurityinsufficient_scope.no_security_scope0 of 1
3noApprovalapproval_required.no_approval0 of 1
4otherwisedisabling1 of 1
roleLacksPermission
caller == 'noSecurityRole'
scopeLacksSecurity
caller == 'writeScopeOnly'
noApproval
approval == 'missing'

Check the plugin and its state, and turn it off

#IfThen
1the path names no installed pluginRefuse a plugin the deployment did not load
2the state moved since the caller read itRefuse a request written against another state
3the row is written off, its config kept, conditional on its versionThe plugin is off again, its config kept

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1pluginUnknownnot_found.plugin_unknown0 of 1
2stateMovedprecondition_failed.state_moved0 of 1
3otherwisepluginOffAgain1 of 1
pluginUnknown
plugin == 'unknown'
stateMoved
stateMatch == 'moved'

Every way it can end

It ends withWhenWhat happens
Answer the plugin's route as a path no route serves
ends here
Its role holds plugins.security and its token carries qauth:admin:security, QAuth-Approval names an approval filed for exactly this request by this caller, approved and unexpired, the path names a plugin this deployment loaded at boot, the body's version, and If-Match when present, name the version of the plugin state the handler reads, the realm's config of the plugin, its secrets decrypted, passes the plugin's config schema
  1. The plugin is off in the realm: a browser requests one of the plugin's routes under /upstream/<type> on the realm host
Hand the request to the plugin's route
ends here
Its role holds plugins.security and its token carries qauth:admin:security, QAuth-Approval names an approval filed for exactly this request by this caller, approved and unexpired, the path names a plugin this deployment loaded at boot, the body's version, and If-Match when present, name the version of the plugin state the handler reads, the realm's config of the plugin, its secrets decrypted, passes the plugin's config schema
  1. The plugin is off in the realm: an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/enable
  2. Check the permission and the approval of the enable: the caller holds the permission and the approval, which the handler transaction consumes
  3. Check the plugin, its state and its config, and turn it on: the row is written enabled, conditional on its version, and the audit row records the change
  4. The plugin is on in the realm: a browser requests one of the plugin's routes on the realm host
Refuse a caller whose role lacks the permission
ends here
Its role holds no security permission, such as an editor, whatever its token carries, QAuth-Approval names an approval filed for exactly this request by this caller, approved and unexpired, the path names a plugin this deployment loaded at boot, the body's version, and If-Match when present, name the version of the plugin state the handler reads, the realm's config of the plugin, its secrets decrypted, passes the plugin's config schema
  1. The plugin is off in the realm: an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/enable
  2. Check the permission and the approval of the enable: the role lacks the permission: no token would pass, and the challenge names no scope
Refuse a token without qauth:admin:security, naming the scope
ends here
Its role holds plugins.security, and its token carries qauth:admin:write but not qauth:admin:security, QAuth-Approval names an approval filed for exactly this request by this caller, approved and unexpired, the path names a plugin this deployment loaded at boot, the body's version, and If-Match when present, name the version of the plugin state the handler reads, the realm's config of the plugin, its secrets decrypted, passes the plugin's config schema
  1. The plugin is off in the realm: an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/enable
  2. Check the permission and the approval of the enable: only the token withholds the permission: qauth:admin:write is refused
Refuse a security operation without its approval
ends here
Its role holds plugins.security and its token carries qauth:admin:security, it names none, or one for another request, or one not approved, expired or used, the path names a plugin this deployment loaded at boot, the body's version, and If-Match when present, name the version of the plugin state the handler reads, the realm's config of the plugin, its secrets decrypted, passes the plugin's config schema
  1. The plugin is off in the realm: an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/enable
  2. Check the permission and the approval of the enable: no approval for exactly this request: the operation never runs
Refuse a plugin the deployment did not load
ends here
Its role holds plugins.security and its token carries qauth:admin:security, QAuth-Approval names an approval filed for exactly this request by this caller, approved and unexpired, it names no loaded plugin, the body's version, and If-Match when present, name the version of the plugin state the handler reads, the realm's config of the plugin, its secrets decrypted, passes the plugin's config schema
  1. The plugin is off in the realm: an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/enable
  2. Check the permission and the approval of the enable: the caller holds the permission and the approval, which the handler transaction consumes
  3. Check the plugin, its state and its config, and turn it on: the path names no installed plugin
Refuse a request written against another state
ends here
Its role holds plugins.security and its token carries qauth:admin:security, QAuth-Approval names an approval filed for exactly this request by this caller, approved and unexpired, the path names a plugin this deployment loaded at boot, the body or If-Match names another version: the state changed since the caller read it, or since its approval was filed, the realm's config of the plugin, its secrets decrypted, passes the plugin's config schema
  1. The plugin is off in the realm: an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/enable
  2. Check the permission and the approval of the enable: the caller holds the permission and the approval, which the handler transaction consumes
  3. Check the plugin, its state and its config, and turn it on: the state moved since the caller read it, or its approver saw it: the body names the version the approval binds
Refuse to enable a plugin whose config its schema refuses
ends here
Its role holds plugins.security and its token carries qauth:admin:security, QAuth-Approval names an approval filed for exactly this request by this caller, approved and unexpired, the path names a plugin this deployment loaded at boot, the body's version, and If-Match when present, name the version of the plugin state the handler reads, it does not, or the realm never configured the plugin
  1. The plugin is off in the realm: an admin sends POST /admin/v1-rc.0/realms/{realmId}/plugins/{plugin}/enable
  2. Check the permission and the approval of the enable: the caller holds the permission and the approval, which the handler transaction consumes
  3. Check the plugin, its state and its config, and turn it on: the realm's config does not pass the plugin's schema: a plugin is on only with a config it accepts

Each row is also a generated test: set the starting situation, send the events, expect the ending.

What is guaranteed

These hold in all 204 situations the model can reach, from 48 different starting situations.

Exact rules
IdNever
write-scope-never-enablesenabled and scopeLacksSecurity
no-enable-without-approvalenabled and noApproval
no-enable-with-a-refused-configenabled and configInvalid
an-off-plugin-runs-nothingstate == 'bad_request.no_pending_sign_in' and not enabled
a-refused-disable-leaves-it-onstate == 'pluginOffAgain' and (roleLacksPermission or scopeLacksSecurity or noApproval)

Reference

All transitions (20)
FromEventGuardToEffectsTaken
pluginOffENABLEauthorizingEnableyes
pluginOffROUTE_REQUESTnot_found.plugin_offyes
authorizingEnablerule 1roleLacksPermissionforbidden.no_permissionyes
authorizingEnablerule 2scopeLacksSecurityinsufficient_scope.no_security_scopeyes
authorizingEnablerule 3noApprovalapproval_required.no_approvalyes
authorizingEnablerule 4enablingyes
enablingrule 1pluginUnknownnot_found.plugin_unknownyes
enablingrule 2stateMovedprecondition_failed.state_movedyes
enablingrule 3configInvalidbad_request.config_invalidyes
enablingrule 4pluginOnturnOnyes
pluginOnROUTE_REQUESTbad_request.no_pending_sign_inyes
pluginOnDISABLEauthorizingDisableyes
authorizingDisablerule 1roleLacksPermissionforbidden.no_permissionnever
authorizingDisablerule 2scopeLacksSecurityinsufficient_scope.no_security_scopenever
authorizingDisablerule 3noApprovalapproval_required.no_approvalnever
authorizingDisablerule 4disablingyes
disablingrule 1pluginUnknownnot_found.plugin_unknownnever
disablingrule 2stateMovedprecondition_failed.state_movednever
disablingrule 3pluginOffAgainturnOffyes
pluginOffAgainROUTE_REQUESTnot_found.plugin_offyes