mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 05:23:01 +00:00
persist roles through oauth process
- Attach role limit to OAuthClient - Attach authorized roles to OAuthCode - pass roles from code to API token on completion standard 'scopes' in oauth process are matched against our 'roles' instead of our low-level scopes
This commit is contained in:
@@ -149,7 +149,12 @@ class JupyterHubRequestValidator(RequestValidator):
|
||||
- Resource Owner Password Credentials Grant
|
||||
- Client Credentials grant
|
||||
"""
|
||||
return ['identify']
|
||||
orm_client = (
|
||||
self.db.query(orm.OAuthClient).filter_by(identifier=client_id).first()
|
||||
)
|
||||
if orm_client is None:
|
||||
raise ValueError("No such client: %s" % client_id)
|
||||
return [role.name for role in orm_client.allowed_roles]
|
||||
|
||||
def get_original_scopes(self, refresh_token, request, *args, **kwargs):
|
||||
"""Get the list of scopes associated with the refresh token.
|
||||
@@ -249,8 +254,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
||||
code=code['code'],
|
||||
# oauth has 5 minutes to complete
|
||||
expires_at=int(orm.OAuthCode.now() + 300),
|
||||
# TODO: persist oauth scopes
|
||||
# scopes=request.scopes,
|
||||
roles=request._jupyterhub_roles,
|
||||
user=request.user.orm_user,
|
||||
redirect_uri=orm_client.redirect_uri,
|
||||
session_id=request.session_id,
|
||||
@@ -324,10 +328,6 @@ class JupyterHubRequestValidator(RequestValidator):
|
||||
"""
|
||||
log_token = {}
|
||||
log_token.update(token)
|
||||
scopes = token['scope'].split(' ')
|
||||
# TODO:
|
||||
if scopes != ['identify']:
|
||||
raise ValueError("Only 'identify' scope is supported")
|
||||
# redact sensitive keys in log
|
||||
for key in ('access_token', 'refresh_token', 'state'):
|
||||
if key in token:
|
||||
@@ -335,6 +335,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
||||
if isinstance(value, str):
|
||||
log_token[key] = 'REDACTED'
|
||||
app_log.debug("Saving bearer token %s", log_token)
|
||||
|
||||
if request.user is None:
|
||||
raise ValueError("No user for access token: %s" % request.user)
|
||||
client = (
|
||||
@@ -342,9 +343,6 @@ class JupyterHubRequestValidator(RequestValidator):
|
||||
.filter_by(identifier=request.client.client_id)
|
||||
.first()
|
||||
)
|
||||
# FIXME: pick a role
|
||||
# this will be empty for now
|
||||
roles = list(self.db.query(orm.Role).filter_by(name='identify'))
|
||||
# FIXME: support refresh tokens
|
||||
# These should be in a new table
|
||||
token.pop("refresh_token", None)
|
||||
@@ -353,7 +351,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
||||
orm.APIToken.new(
|
||||
client_id=client.identifier,
|
||||
expires_in=token['expires_in'],
|
||||
roles=roles,
|
||||
roles=request._jupyterhub_roles,
|
||||
token=token['access_token'],
|
||||
session_id=request.session_id,
|
||||
user=request.user,
|
||||
@@ -455,9 +453,8 @@ class JupyterHubRequestValidator(RequestValidator):
|
||||
return False
|
||||
request.user = orm_code.user
|
||||
request.session_id = orm_code.session_id
|
||||
# TODO: record state on oauth codes
|
||||
# TODO: specify scopes
|
||||
request.scopes = ['identify']
|
||||
request.scopes = [role.name for role in orm_code.roles]
|
||||
request._jupyterhub_roles = orm_code.roles
|
||||
return True
|
||||
|
||||
def validate_grant_type(
|
||||
@@ -553,6 +550,34 @@ class JupyterHubRequestValidator(RequestValidator):
|
||||
- Resource Owner Password Credentials Grant
|
||||
- Client Credentials Grant
|
||||
"""
|
||||
orm_client = (
|
||||
self.db.query(orm.OAuthClient).filter_by(identifier=client_id).one_or_none()
|
||||
)
|
||||
if orm_client is None:
|
||||
app_log.warning("No such oauth client %s", client_id)
|
||||
return False
|
||||
client_allowed_roles = {role.name: role for role in orm_client.allowed_roles}
|
||||
# explicitly allow 'identify', which was the only allowed scope previously
|
||||
# requesting 'identify' gets no actual permissions other than self-identification
|
||||
client_allowed_roles.setdefault('identify', None)
|
||||
roles = []
|
||||
requested_roles = set(scopes)
|
||||
disallowed_roles = requested_roles.difference(client_allowed_roles)
|
||||
if disallowed_roles:
|
||||
app_log.error(
|
||||
f"Role(s) not allowed for {client_id}: {','.join(disallowed_roles)}"
|
||||
)
|
||||
return False
|
||||
|
||||
# store resolved roles on request
|
||||
app_log.debug(
|
||||
f"Allowing request for role(s) for {client_id}: {','.join(requested_roles) or '[]'}"
|
||||
)
|
||||
request._jupyterhub_roles = [
|
||||
client_allowed_roles[name]
|
||||
for name in requested_roles
|
||||
if client_allowed_roles[name] is not None
|
||||
]
|
||||
return True
|
||||
|
||||
|
||||
|
@@ -137,43 +137,34 @@ class Server(Base):
|
||||
return "<Server(%s:%s)>" % (self.ip, self.port)
|
||||
|
||||
|
||||
# user:role many:many mapping table
|
||||
user_role_map = Table(
|
||||
'user_role_map',
|
||||
Base.metadata,
|
||||
Column('user_id', ForeignKey('users.id', ondelete='CASCADE'), primary_key=True),
|
||||
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
|
||||
)
|
||||
# lots of things have roles
|
||||
# mapping tables are the same for all of them
|
||||
|
||||
# service:role many:many mapping table
|
||||
service_role_map = Table(
|
||||
'service_role_map',
|
||||
_role_map_tables = []
|
||||
|
||||
for has_role in (
|
||||
'user',
|
||||
'group',
|
||||
'service',
|
||||
'api_token',
|
||||
'oauth_client',
|
||||
'oauth_code',
|
||||
):
|
||||
role_map = Table(
|
||||
f'{has_role}_role_map',
|
||||
Base.metadata,
|
||||
Column(
|
||||
'service_id', ForeignKey('services.id', ondelete='CASCADE'), primary_key=True
|
||||
),
|
||||
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
|
||||
)
|
||||
|
||||
# token:role many:many mapping table
|
||||
api_token_role_map = Table(
|
||||
'api_token_role_map',
|
||||
Base.metadata,
|
||||
Column(
|
||||
'api_token_id',
|
||||
ForeignKey('api_tokens.id', ondelete='CASCADE'),
|
||||
f'{has_role}_id',
|
||||
ForeignKey(f'{has_role}s.id', ondelete='CASCADE'),
|
||||
primary_key=True,
|
||||
),
|
||||
Column(
|
||||
'role_id',
|
||||
ForeignKey('roles.id', ondelete='CASCADE'),
|
||||
primary_key=True,
|
||||
),
|
||||
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
|
||||
)
|
||||
|
||||
# group:role many:many mapping table
|
||||
group_role_map = Table(
|
||||
'group_role_map',
|
||||
Base.metadata,
|
||||
Column('group_id', ForeignKey('groups.id', ondelete='CASCADE'), primary_key=True),
|
||||
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
|
||||
)
|
||||
_role_map_tables.append(role_map)
|
||||
|
||||
|
||||
class Role(Base):
|
||||
@@ -714,6 +705,8 @@ class OAuthCode(Expiring, Base):
|
||||
# state = Column(Unicode(1023))
|
||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
|
||||
|
||||
roles = relationship('Role', secondary='oauth_code_role_map')
|
||||
|
||||
@staticmethod
|
||||
def now():
|
||||
return datetime.utcnow().timestamp()
|
||||
@@ -745,6 +738,10 @@ class OAuthClient(Base):
|
||||
)
|
||||
codes = relationship(OAuthCode, backref='client', cascade='all, delete-orphan')
|
||||
|
||||
# these are the roles an oauth client is allowed to request
|
||||
# *not* the roles of the client itself
|
||||
allowed_roles = relationship('Role', secondary='oauth_client_role_map')
|
||||
|
||||
|
||||
# General database utilities
|
||||
|
||||
|
Reference in New Issue
Block a user