User scope objects everywhere. Switch scope objects to namedtuples. Pass the user when validating whether the user has authorized such scopes in the past. Make sure we calculate the scope string using all user scopes form all previously granted tokens.
This commit is contained in:
parent
c93c62600d
commit
3b7b12085d
6 changed files with 103 additions and 76 deletions
|
@ -4,6 +4,8 @@ from flask.ext.principal import identity_loaded, Permission, Identity, identity_
|
||||||
from collections import namedtuple, defaultdict
|
from collections import namedtuple, defaultdict
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
import scopes
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
|
@ -31,22 +33,22 @@ TEAM_REPO_ROLES = {
|
||||||
|
|
||||||
SCOPE_MAX_REPO_ROLES = defaultdict(lambda: None)
|
SCOPE_MAX_REPO_ROLES = defaultdict(lambda: None)
|
||||||
SCOPE_MAX_REPO_ROLES.update({
|
SCOPE_MAX_REPO_ROLES.update({
|
||||||
'repo:read': 'read',
|
scopes.READ_REPO: 'read',
|
||||||
'repo:write': 'write',
|
scopes.WRITE_REPO: 'write',
|
||||||
'repo:admin': 'admin',
|
scopes.ADMIN_REPO: 'admin',
|
||||||
'direct_user_login': 'admin',
|
scopes.DIRECT_LOGIN: 'admin',
|
||||||
})
|
})
|
||||||
|
|
||||||
SCOPE_MAX_TEAM_ROLES = defaultdict(lambda: None)
|
SCOPE_MAX_TEAM_ROLES = defaultdict(lambda: None)
|
||||||
SCOPE_MAX_TEAM_ROLES.update({
|
SCOPE_MAX_TEAM_ROLES.update({
|
||||||
'repo:create': 'creator',
|
scopes.CREATE_REPO: 'creator',
|
||||||
'direct_user_login': 'admin',
|
scopes.DIRECT_LOGIN: 'admin',
|
||||||
})
|
})
|
||||||
|
|
||||||
SCOPE_MAX_USER_ROLES = defaultdict(lambda: None)
|
SCOPE_MAX_USER_ROLES = defaultdict(lambda: None)
|
||||||
SCOPE_MAX_USER_ROLES.update({
|
SCOPE_MAX_USER_ROLES.update({
|
||||||
'user:read': 'admin',
|
scopes.READ_USER: 'read',
|
||||||
'direct_user_login': 'admin',
|
scopes.DIRECT_LOGIN: 'admin',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
106
auth/scopes.py
106
auth/scopes.py
|
@ -1,49 +1,65 @@
|
||||||
READ_REPO = {
|
from collections import namedtuple
|
||||||
'scope': 'repo:read',
|
|
||||||
'icon': 'fa-hdd-o',
|
|
||||||
'title': 'View all visible repositories',
|
|
||||||
'description': ('This application will be able to view and pull all repositories visible to the '
|
|
||||||
'granting user or robot account')
|
|
||||||
}
|
|
||||||
|
|
||||||
WRITE_REPO = {
|
|
||||||
'scope': 'repo:write',
|
|
||||||
'icon': 'fa-hdd-o',
|
|
||||||
'title': 'Read/Write to any accessible repositories',
|
|
||||||
'description': ('This application will be able to view, push and pull to all repositories to '
|
|
||||||
'which the granting user or robot account has write access')
|
|
||||||
}
|
|
||||||
|
|
||||||
ADMIN_REPO = {
|
Scope = namedtuple('scope', ['scope', 'icon', 'title', 'description'])
|
||||||
'scope': 'repo:admin',
|
|
||||||
'icon': 'fa-hdd-o',
|
|
||||||
'title': 'Administer Repositories',
|
|
||||||
'description': ('This application will have administrator access to all repositories to which '
|
|
||||||
'the granting user or robot account has access')
|
|
||||||
}
|
|
||||||
|
|
||||||
CREATE_REPO = {
|
|
||||||
'scope': 'repo:create',
|
|
||||||
'icon': 'fa-plus',
|
|
||||||
'title': 'Create Repositories',
|
|
||||||
'description': ('This application will be able to create repositories in to any namespaces that '
|
|
||||||
'the granting user or robot account is allowed to create repositories')
|
|
||||||
}
|
|
||||||
|
|
||||||
READ_USER = {
|
READ_REPO = Scope(scope='repo:read',
|
||||||
'scope': 'user:read',
|
icon='fa-hdd-o',
|
||||||
'icon': 'fa-user',
|
title='View all visible repositories',
|
||||||
'title': 'Read User Information',
|
description=('This application will be able to view and pull all repositories '
|
||||||
'description': ('This application will be able to read user information such as username and '
|
'visible to the granting user or robot account'))
|
||||||
'email address.'),
|
|
||||||
}
|
|
||||||
|
|
||||||
ALL_SCOPES = {scope['scope']:scope for scope in (READ_REPO, WRITE_REPO, ADMIN_REPO, CREATE_REPO,
|
WRITE_REPO = Scope(scope='repo:write',
|
||||||
READ_USER)}
|
icon='fa-hdd-o',
|
||||||
|
title='Read/Write to any accessible repositories',
|
||||||
|
description=('This application will be able to view, push and pull to all '
|
||||||
|
'repositories to which the granting user or robot account has '
|
||||||
|
'write access'))
|
||||||
|
|
||||||
|
ADMIN_REPO = Scope(scope='repo:admin',
|
||||||
|
icon='fa-hdd-o',
|
||||||
|
title='Administer Repositories',
|
||||||
|
description=('This application will have administrator access to all '
|
||||||
|
'repositories to which the granting user or robot account has '
|
||||||
|
'access'))
|
||||||
|
|
||||||
|
CREATE_REPO = Scope(scope='repo:create',
|
||||||
|
icon='fa-plus',
|
||||||
|
title='Create Repositories',
|
||||||
|
description=('This application will be able to create repositories in to any '
|
||||||
|
'namespaces that the granting user or robot account is allowed to '
|
||||||
|
'create repositories'))
|
||||||
|
|
||||||
|
READ_USER = Scope(scope= 'user:read',
|
||||||
|
icon='fa-user',
|
||||||
|
title='Read User Information',
|
||||||
|
description=('This application will be able to read user information such as '
|
||||||
|
'username and email address.'))
|
||||||
|
|
||||||
|
|
||||||
|
DIRECT_LOGIN = Scope(scope='direct_user_login',
|
||||||
|
icon='fa-exclamation-triangle',
|
||||||
|
title='Full Access',
|
||||||
|
description=('This scope should not be available to OAuth applications. '
|
||||||
|
'Never approve a request for this scope!'))
|
||||||
|
|
||||||
|
|
||||||
|
ALL_SCOPES = {scope.scope:scope for scope in (READ_REPO, WRITE_REPO, ADMIN_REPO, CREATE_REPO,
|
||||||
|
READ_USER)}
|
||||||
|
|
||||||
|
IMPLIED_SCOPES = {
|
||||||
|
ADMIN_REPO: {ADMIN_REPO, WRITE_REPO, READ_REPO},
|
||||||
|
WRITE_REPO: {WRITE_REPO, READ_REPO},
|
||||||
|
READ_REPO: {READ_REPO},
|
||||||
|
CREATE_REPO: {CREATE_REPO},
|
||||||
|
READ_USER: {READ_USER},
|
||||||
|
None: set(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def scopes_from_scope_string(scopes):
|
def scopes_from_scope_string(scopes):
|
||||||
return {ALL_SCOPES.get(scope, {}).get('scope', None) for scope in scopes.split(',')}
|
return {ALL_SCOPES.get(scope, None) for scope in scopes.split(',')}
|
||||||
|
|
||||||
|
|
||||||
def validate_scope_string(scopes):
|
def validate_scope_string(scopes):
|
||||||
|
@ -56,8 +72,10 @@ def is_subset_string(full_string, expected_string):
|
||||||
in full_string.
|
in full_string.
|
||||||
"""
|
"""
|
||||||
full_scopes = scopes_from_scope_string(full_string)
|
full_scopes = scopes_from_scope_string(full_string)
|
||||||
|
full_implied_scopes = set.union(*[IMPLIED_SCOPES[scope] for scope in full_scopes])
|
||||||
expected_scopes = scopes_from_scope_string(expected_string)
|
expected_scopes = scopes_from_scope_string(expected_string)
|
||||||
return expected_scopes.issubset(full_scopes)
|
return expected_scopes.issubset(full_implied_scopes)
|
||||||
|
|
||||||
|
|
||||||
def get_scope_information(scopes_string):
|
def get_scope_information(scopes_string):
|
||||||
scopes = scopes_from_scope_string(scopes_string)
|
scopes = scopes_from_scope_string(scopes_string)
|
||||||
|
@ -65,10 +83,10 @@ def get_scope_information(scopes_string):
|
||||||
for scope in scopes:
|
for scope in scopes:
|
||||||
if scope:
|
if scope:
|
||||||
scope_info.append({
|
scope_info.append({
|
||||||
'title': ALL_SCOPES[scope]['title'],
|
'title': scope.title,
|
||||||
'scope': ALL_SCOPES[scope]['scope'],
|
'scope': scope.scope,
|
||||||
'description': ALL_SCOPES[scope]['description'],
|
'description': scope.description,
|
||||||
'icon': ALL_SCOPES[scope]['icon'],
|
'icon': scope.icon,
|
||||||
})
|
})
|
||||||
|
|
||||||
return scope_info
|
return scope_info
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from oauth2lib.provider import AuthorizationProvider
|
from oauth2lib.provider import AuthorizationProvider
|
||||||
from oauth2lib import utils
|
from oauth2lib import utils
|
||||||
|
@ -6,6 +8,9 @@ from data.database import OAuthApplication, OAuthAuthorizationCode, OAuthAccessT
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseAuthorizationProvider(AuthorizationProvider):
|
class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||||
def get_authorized_user(self):
|
def get_authorized_user(self):
|
||||||
raise NotImplementedError('Subclasses must fill in the ability to get the authorized_user.')
|
raise NotImplementedError('Subclasses must fill in the ability to get the authorized_user.')
|
||||||
|
@ -41,28 +46,25 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||||
def validate_access(self):
|
def validate_access(self):
|
||||||
return self.get_authorized_user() is not None
|
return self.get_authorized_user() is not None
|
||||||
|
|
||||||
def lookup_access_token(self, client_id):
|
def load_authorized_scope_string(self, client_id, username):
|
||||||
try:
|
found = (OAuthAccessToken
|
||||||
found = (OAuthAccessToken
|
.select()
|
||||||
.select()
|
.join(OAuthApplication)
|
||||||
.join(OAuthApplication)
|
.switch(OAuthAccessToken)
|
||||||
.where(OAuthApplication.client_id == client_id)
|
.join(User)
|
||||||
.get())
|
.where(OAuthApplication.client_id == client_id, User.username == username,
|
||||||
return found
|
OAuthAccessToken.expires_at > datetime.now()))
|
||||||
except OAuthAccessToken.DoesNotExist:
|
found = list(found)
|
||||||
return None
|
logger.debug('Found %s matching tokens.', len(found))
|
||||||
|
long_scope_string = ','.join([token.scope for token in found])
|
||||||
|
logger.debug('Computed long scope string: %s', long_scope_string)
|
||||||
|
return long_scope_string
|
||||||
|
|
||||||
def validate_has_scopes(self, client_id, scope):
|
def validate_has_scopes(self, client_id, username, scope):
|
||||||
access_token = self.lookup_access_token(client_id)
|
long_scope_string = self.load_authorized_scope_string(client_id, username)
|
||||||
if not access_token:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Make sure the token is not expired.
|
|
||||||
if access_token.expires_at <= datetime.now():
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Make sure the token contains the given scopes (at least).
|
# Make sure the token contains the given scopes (at least).
|
||||||
return scopes.is_subset_string(access_token.scope, scope)
|
return scopes.is_subset_string(long_scope_string, scope)
|
||||||
|
|
||||||
def from_authorization_code(self, client_id, code, scope):
|
def from_authorization_code(self, client_id, code, scope):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -197,7 +197,7 @@ def require_user_permission(permission_class, scope=None):
|
||||||
if not user:
|
if not user:
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
logger.debug('Checking permission %s for user', permission_class, user.username)
|
logger.debug('Checking permission %s for user %s', permission_class, user.username)
|
||||||
permission = permission_class(user.username)
|
permission = permission_class(user.username)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
|
@ -212,7 +212,7 @@ require_user_admin = require_user_permission(UserAdminPermission, None)
|
||||||
|
|
||||||
def require_scope(scope_object):
|
def require_scope(scope_object):
|
||||||
def wrapper(func):
|
def wrapper(func):
|
||||||
@add_method_metadata('oauth2_scope', scope_object['scope'])
|
@add_method_metadata('oauth2_scope', scope_object)
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
|
@ -100,7 +100,11 @@ def swagger_route_data(include_internal=False, compact=False):
|
||||||
scope = method_metadata(method, 'oauth2_scope')
|
scope = method_metadata(method, 'oauth2_scope')
|
||||||
if scope and not compact:
|
if scope and not compact:
|
||||||
new_operation['authorizations'] = {
|
new_operation['authorizations'] = {
|
||||||
'oauth2': [scope],
|
'oauth2': [
|
||||||
|
{
|
||||||
|
'scope': scope.scope
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
internal = method_metadata(method, 'internal')
|
internal = method_metadata(method, 'internal')
|
||||||
|
@ -148,7 +152,7 @@ def swagger_route_data(include_internal=False, compact=False):
|
||||||
},
|
},
|
||||||
'authorizations': {
|
'authorizations': {
|
||||||
'oauth2': {
|
'oauth2': {
|
||||||
'scopes': list(scopes.ALL_SCOPES.values()),
|
'scopes': [scope._asdict() for scope in scopes.ALL_SCOPES.values()],
|
||||||
'grantTypes': {
|
'grantTypes': {
|
||||||
"implicit": {
|
"implicit": {
|
||||||
"tokenName": "access_token",
|
"tokenName": "access_token",
|
||||||
|
|
|
@ -277,7 +277,8 @@ def request_authorization_code():
|
||||||
redirect_uri = request.args.get('redirect_uri', None)
|
redirect_uri = request.args.get('redirect_uri', None)
|
||||||
scope = request.args.get('scope', None)
|
scope = request.args.get('scope', None)
|
||||||
|
|
||||||
if not provider.validate_has_scopes(client_id, scope):
|
if (not current_user.is_authenticated() or
|
||||||
|
not provider.validate_has_scopes(client_id, current_user.db_user().username, scope)):
|
||||||
if not provider.validate_redirect_uri(client_id, redirect_uri):
|
if not provider.validate_redirect_uri(client_id, redirect_uri):
|
||||||
abort(404)
|
abort(404)
|
||||||
return
|
return
|
||||||
|
|
Reference in a new issue