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
|
- Resource Owner Password Credentials Grant
|
||||||
- Client 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):
|
def get_original_scopes(self, refresh_token, request, *args, **kwargs):
|
||||||
"""Get the list of scopes associated with the refresh token.
|
"""Get the list of scopes associated with the refresh token.
|
||||||
@@ -249,8 +254,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
code=code['code'],
|
code=code['code'],
|
||||||
# oauth has 5 minutes to complete
|
# oauth has 5 minutes to complete
|
||||||
expires_at=int(orm.OAuthCode.now() + 300),
|
expires_at=int(orm.OAuthCode.now() + 300),
|
||||||
# TODO: persist oauth scopes
|
roles=request._jupyterhub_roles,
|
||||||
# scopes=request.scopes,
|
|
||||||
user=request.user.orm_user,
|
user=request.user.orm_user,
|
||||||
redirect_uri=orm_client.redirect_uri,
|
redirect_uri=orm_client.redirect_uri,
|
||||||
session_id=request.session_id,
|
session_id=request.session_id,
|
||||||
@@ -324,10 +328,6 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
"""
|
"""
|
||||||
log_token = {}
|
log_token = {}
|
||||||
log_token.update(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
|
# redact sensitive keys in log
|
||||||
for key in ('access_token', 'refresh_token', 'state'):
|
for key in ('access_token', 'refresh_token', 'state'):
|
||||||
if key in token:
|
if key in token:
|
||||||
@@ -335,6 +335,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
log_token[key] = 'REDACTED'
|
log_token[key] = 'REDACTED'
|
||||||
app_log.debug("Saving bearer token %s", log_token)
|
app_log.debug("Saving bearer token %s", log_token)
|
||||||
|
|
||||||
if request.user is None:
|
if request.user is None:
|
||||||
raise ValueError("No user for access token: %s" % request.user)
|
raise ValueError("No user for access token: %s" % request.user)
|
||||||
client = (
|
client = (
|
||||||
@@ -342,9 +343,6 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
.filter_by(identifier=request.client.client_id)
|
.filter_by(identifier=request.client.client_id)
|
||||||
.first()
|
.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
|
# FIXME: support refresh tokens
|
||||||
# These should be in a new table
|
# These should be in a new table
|
||||||
token.pop("refresh_token", None)
|
token.pop("refresh_token", None)
|
||||||
@@ -353,7 +351,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
orm.APIToken.new(
|
orm.APIToken.new(
|
||||||
client_id=client.identifier,
|
client_id=client.identifier,
|
||||||
expires_in=token['expires_in'],
|
expires_in=token['expires_in'],
|
||||||
roles=roles,
|
roles=request._jupyterhub_roles,
|
||||||
token=token['access_token'],
|
token=token['access_token'],
|
||||||
session_id=request.session_id,
|
session_id=request.session_id,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
@@ -455,9 +453,8 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
return False
|
return False
|
||||||
request.user = orm_code.user
|
request.user = orm_code.user
|
||||||
request.session_id = orm_code.session_id
|
request.session_id = orm_code.session_id
|
||||||
# TODO: record state on oauth codes
|
request.scopes = [role.name for role in orm_code.roles]
|
||||||
# TODO: specify scopes
|
request._jupyterhub_roles = orm_code.roles
|
||||||
request.scopes = ['identify']
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def validate_grant_type(
|
def validate_grant_type(
|
||||||
@@ -553,6 +550,34 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
- Resource Owner Password Credentials Grant
|
- Resource Owner Password Credentials Grant
|
||||||
- Client 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
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@@ -137,43 +137,34 @@ class Server(Base):
|
|||||||
return "<Server(%s:%s)>" % (self.ip, self.port)
|
return "<Server(%s:%s)>" % (self.ip, self.port)
|
||||||
|
|
||||||
|
|
||||||
# user:role many:many mapping table
|
# lots of things have roles
|
||||||
user_role_map = Table(
|
# mapping tables are the same for all of them
|
||||||
'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),
|
|
||||||
)
|
|
||||||
|
|
||||||
# service:role many:many mapping table
|
_role_map_tables = []
|
||||||
service_role_map = Table(
|
|
||||||
'service_role_map',
|
for has_role in (
|
||||||
|
'user',
|
||||||
|
'group',
|
||||||
|
'service',
|
||||||
|
'api_token',
|
||||||
|
'oauth_client',
|
||||||
|
'oauth_code',
|
||||||
|
):
|
||||||
|
role_map = Table(
|
||||||
|
f'{has_role}_role_map',
|
||||||
Base.metadata,
|
Base.metadata,
|
||||||
Column(
|
Column(
|
||||||
'service_id', ForeignKey('services.id', ondelete='CASCADE'), primary_key=True
|
f'{has_role}_id',
|
||||||
),
|
ForeignKey(f'{has_role}s.id', ondelete='CASCADE'),
|
||||||
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
|
primary_key=True,
|
||||||
)
|
),
|
||||||
|
Column(
|
||||||
# token:role many:many mapping table
|
'role_id',
|
||||||
api_token_role_map = Table(
|
ForeignKey('roles.id', ondelete='CASCADE'),
|
||||||
'api_token_role_map',
|
|
||||||
Base.metadata,
|
|
||||||
Column(
|
|
||||||
'api_token_id',
|
|
||||||
ForeignKey('api_tokens.id', ondelete='CASCADE'),
|
|
||||||
primary_key=True,
|
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):
|
class Role(Base):
|
||||||
@@ -714,6 +705,8 @@ class OAuthCode(Expiring, Base):
|
|||||||
# state = Column(Unicode(1023))
|
# state = Column(Unicode(1023))
|
||||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
|
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
|
||||||
|
|
||||||
|
roles = relationship('Role', secondary='oauth_code_role_map')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def now():
|
def now():
|
||||||
return datetime.utcnow().timestamp()
|
return datetime.utcnow().timestamp()
|
||||||
@@ -745,6 +738,10 @@ class OAuthClient(Base):
|
|||||||
)
|
)
|
||||||
codes = relationship(OAuthCode, backref='client', cascade='all, delete-orphan')
|
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
|
# General database utilities
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user