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:
Min RK
2021-04-19 12:45:35 +02:00
parent 53f0d88505
commit 4728325bf7
2 changed files with 71 additions and 49 deletions

View File

@@ -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

View File

@@ -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',
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 for has_role in (
api_token_role_map = Table( 'user',
'api_token_role_map', 'group',
Base.metadata, 'service',
Column( 'api_token',
'api_token_id', 'oauth_client',
ForeignKey('api_tokens.id', ondelete='CASCADE'), 'oauth_code',
primary_key=True, ):
), role_map = Table(
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True), f'{has_role}_role_map',
) Base.metadata,
Column(
# group:role many:many mapping table f'{has_role}_id',
group_role_map = Table( ForeignKey(f'{has_role}s.id', ondelete='CASCADE'),
'group_role_map', primary_key=True,
Base.metadata, ),
Column('group_id', ForeignKey('groups.id', ondelete='CASCADE'), primary_key=True), Column(
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True), '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