Admin Scopes Not Given to Users With Admin Role

I attempt to force scopes with z2jh hub config here

hub:
  loadRoles:
    jupyterdevs:
      groups: [JupyterDevs]
      scopes: [admin-ui, admin:users, admin:servers, tokens, admin:groups, list:services, read:services, read:hub, proxy, shutdown, access:services, access:servers, read:roles, read:metrics]
...

I provide groups from my authenticator, my extra spawner config is below. I also force the auth_model into admin = True here.

hub:
  config:
    Authenticator:
      auto_login: true
      username_claim: unique_name
      allow_all: true
      scope:
        - openid
        - profile
        - email
    JupyterHub:
      authenticator_class: azuread
...
  extraFiles:
    spawner_config:
      mountPath: /usr/local/etc/jupyterhub/jupyterhub_config.d/spawner_config.py
class CustomAzureAuth(AzureAdOAuthenticator):
    async def update_auth_model(self, auth_model):
        auth_model['groups'] = []

        #***
        #some custom methods that give me clientperms.get() function, don't worry about this
        #***
        auth_model['groups'] += clientperms.get(auth_model['auth_state']['user']['oid'])

        if 'JupyterDevs' in auth_model['groups']:
            auth_model['admin'] = True
        
        return auth_model

c.JupyterHub.authenticator_class = CustomAzureAuth
c.Authenticator.manage_groups = True
c.AzureAdOAuthenticator.client_id = os.getenv('AZURE_CLIENT_ID')
c.AzureAdOAuthenticator.client_secret = os.getenv('AZURE_CLIENT_SECRET')
c.AzureAdOAuthenticator.oauth_callback_url = os.getenv('AZURE_OAUTH_CALLBACK')
c.AzureAdOAuthenticator.tenant_id = os.getenv('AZURE_TENANT_ID')

When I enter the server, I can make a request to get my user details

> curl http://hub:8081/hub/api/user -H "Authorization: Bearer ${JUPYTERHUB_API_TOKEN}"
{"last_activity": "2023-09-08T22:37:31.398506Z", "admin": true, "kind": "user", "name": "dduke", "groups": ["JupyterDevs", ...], "session_id": null, "scopes": ["access:servers!server=dduke/", "read:users:activity!user=dduke", "read:users:groups!user=dduke", "read:users:name!user=dduke", "users:activity!user=dduke"]}

But as you can see, these scopes don’t look correct.
Just to verify:

> curl http://hub:8081/hub/api/services -H "Authorization: Bearer ${JUPYTERHUB_API_TOKEN}"
{"status": 403, "message": "Action is not authorized with current scopes; requires any of [list:services]"}

If I make my way onto the hub, and do some querying on the sqlite, I see below.

> select users.name as username,
        groups.name as group_name,
        roles.name as role_name,
        api_tokens.scopes
    from users
        join group_role_map on group_role_map.group_id = groups.id
        join roles on roles.id = group_role_map.role_id
        join user_group_map on user_group_map.group_id = groups.id
        join groups on groups.id = user_group_map.group_id
        join api_tokens on api_tokens.user_id = users.id
    where username = 'dduke';
dduke|JupyterDevs|admin|["access:servers!server=dduke/", "read:users:groups!user", "read:users:name!user"]
dduke|JupyterDevs|jupyterdevs|["access:servers!server=dduke/", "read:users:groups!user", "read:users:name!user"]
dduke|JupyterDevs|admin|["access:servers!server=dduke/", "users:activity!user"]
dduke|JupyterDevs|jupyterdevs|["access:servers!server=dduke/", "users:activity!user"]
dduke|JupyterDevs|admin|["access:servers!server=dduke/", "users:activity!user"]
dduke|JupyterDevs|jupyterdevs|["access:servers!server=dduke/", "users:activity!user"]

Any thoughts?

JUPYTERHUB_API_TOKEN in the notebook environment only contains the minimal scopes needed for the singleuser-server to interact with JupyterHub- this controlled by the server role:

If you want the additional scopes you can either modify the built-in server role, or create a new API token.

I think they key point here is that the permissions for a server are a subset of the user’s permissions, and the $JUPYTERHUB_API_TOKEN is the server’s token, and the default permissions of a server token are very limited (just access:servers and users:activity).

If you want the server api token to inherit the full permissions of its owner, you can override the server role, granting the ‘inherit’ permissions:

loadRoles:
  server:
    scopes: [inherit]

Or you can be more precise about what permissions you grant the server token.

Additional API tokens can also be issued from the /hub/token page, which default to this inherit permission, without elevating the permissions of the server’s own API token.

Understood - these differences makes sense, I appreciate the clarity provided @manics / @minrk .

I’ll take the steps to inherit as mentioned, but out of my own curiosity - as is/be default, is there not a way to utilize the users permissions that are set? Or does this require generating a token assigned to the user or inheriting the user roles in the server token as mentioned?

is there not a way to utilize the users permissions that are set? Or does this require generating a token assigned to the user or inheriting the user roles in the server token as mentioned?

Generating a token is using the user’s permissions. There is not a distinction. Tokens are the representations of granted permissions. If you want to be able to use all of the user’s permissions, then the token should be issued with the inherit scope, as in the example above.

However, when a browser is talking to a single-user server, there are always two tokens:

  1. the server token, governed by the server role. This token is always owned by the server, and can act on the user’s behalf even while nobody is visiting the server.
  2. the visitor’s token, issued during oauth and stored (encrypted) in a cookie in the browser. Each request to the single-user server is authenticated with this token. This token may be the logical one to use for server extensions that take actions as a result of user operations (e.g. JupyterLab extensions, etc.). oauth_client_allowed_scopes governs the permissions of this token. This token may have more or less permissions than the server token.

More information on tokens and permissions involved in talking to a server in the docs.