mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 14:03:02 +00:00
Merge pull request #3434 from 0mar/server_permissions
Server permissions
This commit is contained in:
@@ -13,6 +13,7 @@ from tornado import web
|
||||
from .. import orm
|
||||
from .. import scopes
|
||||
from ..handlers import BaseHandler
|
||||
from ..user import User
|
||||
from ..utils import isoformat
|
||||
from ..utils import url_path_join
|
||||
|
||||
@@ -70,46 +71,38 @@ class APIHandler(BaseHandler):
|
||||
"""Produce a filter for `*ListAPIHandlers* so that GET method knows which models to return.
|
||||
Filter is a callable that takes a resource name and outputs true or false"""
|
||||
|
||||
try:
|
||||
sub_scope = self.parsed_scopes[req_scope]
|
||||
except AttributeError:
|
||||
raise web.HTTPError(
|
||||
403,
|
||||
"Resource scope %s (that was just accessed) not found in parsed scope model"
|
||||
% req_scope,
|
||||
)
|
||||
def no_access(orm_resource, kind):
|
||||
return False
|
||||
|
||||
if req_scope not in self.parsed_scopes:
|
||||
return no_access
|
||||
sub_scope = self.parsed_scopes[req_scope]
|
||||
|
||||
def has_access_to(orm_resource, kind):
|
||||
"""
|
||||
param orm_resource: User or Service or Group or spawner
|
||||
param kind: 'user' or 'service' or 'group' or 'server'.
|
||||
`kind` could probably be derived from `orm_resource`, problem is Jupyterhub.users.User
|
||||
"""
|
||||
if sub_scope == scopes.Scope.ALL:
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
found_resource = orm_resource.name in sub_scope[kind]
|
||||
except KeyError:
|
||||
found_resource = False
|
||||
if not found_resource: # Try group-based access
|
||||
if kind == 'server' and 'user' in sub_scope:
|
||||
# First check if we have access to user info
|
||||
user_name = orm_resource.user.name
|
||||
found_resource = user_name in sub_scope['user']
|
||||
if not found_resource:
|
||||
# Now check for specific servers:
|
||||
server_format = f"{orm_resource.user / orm_resource.name}"
|
||||
found_resource = server_format in sub_scope[kind]
|
||||
elif 'group' in sub_scope:
|
||||
group_names = set()
|
||||
if kind == 'user':
|
||||
group_names = {group.name for group in orm_resource.groups}
|
||||
elif kind == 'server':
|
||||
group_names = {group.name for group in orm_resource.user.groups}
|
||||
user_in_group = bool(group_names & set(sub_scope['group']))
|
||||
found_resource = user_in_group
|
||||
return found_resource
|
||||
elif orm_resource.name in sub_scope.get(kind, []):
|
||||
return True
|
||||
if kind == 'server':
|
||||
server_format = f"{orm_resource.user.name}/{orm_resource.name}"
|
||||
if server_format in sub_scope.get(kind, []):
|
||||
return True
|
||||
# Fall back on checking if we have user access
|
||||
if orm_resource.user.name in sub_scope.get('user', []):
|
||||
return True
|
||||
# Fall back on checking if we have group access for this user
|
||||
orm_resource = orm_resource.user
|
||||
kind = 'user'
|
||||
if kind == 'user' and 'group' in sub_scope:
|
||||
group_names = {group.name for group in orm_resource.groups}
|
||||
user_in_group = bool(group_names & set(sub_scope['group']))
|
||||
if user_in_group:
|
||||
return True
|
||||
return False
|
||||
|
||||
return has_access_to
|
||||
|
||||
@@ -183,9 +176,8 @@ class APIHandler(BaseHandler):
|
||||
)
|
||||
|
||||
def server_model(self, spawner):
|
||||
"""Get the JSON model for a Spawner"""
|
||||
server_scope = 'read:users:servers'
|
||||
server_state_scope = 'admin:users:server_state'
|
||||
"""Get the JSON model for a Spawner
|
||||
Assume server permission already granted"""
|
||||
model = {
|
||||
'name': spawner.name,
|
||||
'last_activity': isoformat(spawner.orm_spawner.last_activity),
|
||||
@@ -196,11 +188,9 @@ class APIHandler(BaseHandler):
|
||||
'user_options': spawner.user_options,
|
||||
'progress_url': spawner._progress_url,
|
||||
}
|
||||
# First check users, then servers
|
||||
if server_state_scope in self.parsed_scopes:
|
||||
scope_filter = self.get_scope_filter(server_state_scope)
|
||||
if scope_filter(spawner, kind='server'):
|
||||
model['state'] = spawner.get_state()
|
||||
scope_filter = self.get_scope_filter('admin:users:server_state')
|
||||
if scope_filter(spawner, kind='server'):
|
||||
model['state'] = spawner.get_state()
|
||||
return model
|
||||
|
||||
def token_model(self, token):
|
||||
@@ -260,7 +250,6 @@ class APIHandler(BaseHandler):
|
||||
'read:users:activity': {'kind', 'name', 'last_activity'},
|
||||
'read:users:servers': {'kind', 'name', 'servers'},
|
||||
'admin:users:auth_state': {'kind', 'name', 'auth_state'},
|
||||
'admin:users:server_state': {'kind', 'name', 'servers', 'server_state'},
|
||||
}
|
||||
self.log.debug(
|
||||
"Asking for user model of %s with scopes [%s]",
|
||||
@@ -269,52 +258,50 @@ class APIHandler(BaseHandler):
|
||||
)
|
||||
allowed_keys = set()
|
||||
for scope in access_map:
|
||||
if scope in self.parsed_scopes:
|
||||
scope_filter = self.get_scope_filter(scope)
|
||||
if scope_filter(user, kind='user'):
|
||||
allowed_keys |= access_map[scope]
|
||||
scope_filter = self.get_scope_filter(scope)
|
||||
if scope_filter(user, kind='user'):
|
||||
allowed_keys |= access_map[scope]
|
||||
model = {key: model[key] for key in allowed_keys if key in model}
|
||||
if model:
|
||||
if '' in user.spawners and 'pending' in allowed_keys:
|
||||
model['pending'] = user.spawners[''].pending
|
||||
if 'servers' in allowed_keys:
|
||||
servers = model['servers'] = {}
|
||||
for name, spawner in user.spawners.items():
|
||||
# include 'active' servers, not just ready
|
||||
# (this includes pending events)
|
||||
if spawner.active:
|
||||
servers[name] = self.server_model(spawner)
|
||||
|
||||
servers = model['servers'] = {}
|
||||
scope_filter = self.get_scope_filter('read:users:servers')
|
||||
for name, spawner in user.spawners.items():
|
||||
# include 'active' servers, not just ready
|
||||
# (this includes pending events)
|
||||
if spawner.active and scope_filter(spawner, kind='server'):
|
||||
servers[name] = self.server_model(spawner)
|
||||
if not servers:
|
||||
model.pop('servers')
|
||||
return model
|
||||
|
||||
def group_model(self, group):
|
||||
"""Get the JSON model for a Group object"""
|
||||
model = {}
|
||||
req_scope = 'read:groups'
|
||||
if req_scope in self.parsed_scopes:
|
||||
scope_filter = self.get_scope_filter(req_scope)
|
||||
if scope_filter(group, kind='group'):
|
||||
model = {
|
||||
'kind': 'group',
|
||||
'name': group.name,
|
||||
'roles': [r.name for r in group.roles],
|
||||
'users': [u.name for u in group.users],
|
||||
}
|
||||
scope_filter = self.get_scope_filter('read:groups')
|
||||
if scope_filter(group, kind='group'):
|
||||
model = {
|
||||
'kind': 'group',
|
||||
'name': group.name,
|
||||
'roles': [r.name for r in group.roles],
|
||||
'users': [u.name for u in group.users],
|
||||
}
|
||||
return model
|
||||
|
||||
def service_model(self, service):
|
||||
"""Get the JSON model for a Service object"""
|
||||
model = {}
|
||||
req_scope = 'read:services'
|
||||
if req_scope in self.parsed_scopes:
|
||||
scope_filter = self.get_scope_filter(req_scope)
|
||||
if scope_filter(service, kind='service'):
|
||||
model = {
|
||||
'kind': 'service',
|
||||
'name': service.name,
|
||||
'roles': [r.name for r in service.roles],
|
||||
'admin': service.admin,
|
||||
}
|
||||
# todo: Remove once we replace admin flag with role check
|
||||
scope_filter = self.get_scope_filter('read:services')
|
||||
if scope_filter(service, kind='service'):
|
||||
model = {
|
||||
'kind': 'service',
|
||||
'name': service.name,
|
||||
'roles': [r.name for r in service.roles],
|
||||
'admin': service.admin,
|
||||
}
|
||||
# todo: Remove once we replace admin flag with role check
|
||||
return model
|
||||
|
||||
_user_model_types = {
|
||||
|
@@ -40,28 +40,6 @@ class ServiceListAPIHandler(APIHandler):
|
||||
self.write(json.dumps(data))
|
||||
|
||||
|
||||
def admin_or_self(method):
|
||||
"""Decorator for restricting access to either the target service or admin"""
|
||||
"""***Deprecated in favor of RBAC. Use scope-based decorator***"""
|
||||
|
||||
def decorated_method(self, name):
|
||||
current = self.current_user
|
||||
if current is None:
|
||||
raise web.HTTPError(403)
|
||||
if not current.admin:
|
||||
# not admin, maybe self
|
||||
if not isinstance(current, orm.Service):
|
||||
raise web.HTTPError(403)
|
||||
if current.name != name:
|
||||
raise web.HTTPError(403)
|
||||
# raise 404 if not found
|
||||
if name not in self.services:
|
||||
raise web.HTTPError(404)
|
||||
return method(self, name)
|
||||
|
||||
return decorated_method
|
||||
|
||||
|
||||
class ServiceAPIHandler(APIHandler):
|
||||
@needs_scope('read:services')
|
||||
def get(self, service_name):
|
||||
|
@@ -169,24 +169,6 @@ class UserListAPIHandler(APIHandler):
|
||||
self.set_status(201)
|
||||
|
||||
|
||||
def admin_or_self(method):
|
||||
"""Decorator for restricting access to either the target user or admin"""
|
||||
|
||||
def m(self, name, *args, **kwargs):
|
||||
current = self.current_user
|
||||
if current is None:
|
||||
raise web.HTTPError(403)
|
||||
if not (current.name == name or current.admin):
|
||||
raise web.HTTPError(403)
|
||||
|
||||
# raise 404 if not found
|
||||
if not self.find_user(name):
|
||||
raise web.HTTPError(404)
|
||||
return method(self, name, *args, **kwargs)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
class UserAPIHandler(APIHandler):
|
||||
@needs_scope(
|
||||
'read:users',
|
||||
@@ -195,9 +177,7 @@ class UserAPIHandler(APIHandler):
|
||||
'read:users:groups',
|
||||
'read:users:activity',
|
||||
)
|
||||
async def get(
|
||||
self, user_name
|
||||
): # Fixme: Does not work when only server filter is selected
|
||||
async def get(self, user_name):
|
||||
user = self.find_user(user_name)
|
||||
model = self.user_model(user)
|
||||
# auth state will only be shown if the requester is an admin
|
||||
@@ -268,7 +248,7 @@ class UserAPIHandler(APIHandler):
|
||||
|
||||
self.set_status(204)
|
||||
|
||||
@needs_scope('admin:users') # Todo: Change to `users`?
|
||||
@needs_scope('admin:users')
|
||||
async def patch(self, user_name):
|
||||
user = self.find_user(user_name)
|
||||
if user is None:
|
||||
@@ -321,7 +301,7 @@ class UserTokenListAPIHandler(APIHandler):
|
||||
|
||||
self.write(json.dumps({'api_tokens': api_tokens}))
|
||||
|
||||
# Todo: Set to @needs_scope('users:tokens')
|
||||
# @needs_scope('users:tokens') #Todo: needs internal scope checking
|
||||
async def post(self, user_name):
|
||||
body = self.get_json_body() or {}
|
||||
if not isinstance(body, dict):
|
||||
|
@@ -25,6 +25,7 @@ def get_default_roles():
|
||||
'scopes': [
|
||||
'all',
|
||||
'users',
|
||||
'users:servers',
|
||||
'users:tokens',
|
||||
'admin:users',
|
||||
'admin:users:servers',
|
||||
@@ -52,7 +53,7 @@ def get_default_roles():
|
||||
return default_roles
|
||||
|
||||
|
||||
def expand_self_scope(name, read_only=False):
|
||||
def expand_self_scope(name):
|
||||
"""
|
||||
Users have a metascope 'self' that should be expanded to standard user privileges.
|
||||
At the moment that is a user-filtered version (optional read) access to
|
||||
@@ -72,10 +73,7 @@ def expand_self_scope(name, read_only=False):
|
||||
'users:tokens',
|
||||
]
|
||||
read_scope_list = ['read:' + scope for scope in scope_list]
|
||||
if read_only:
|
||||
scope_list = read_scope_list
|
||||
else:
|
||||
scope_list.extend(read_scope_list)
|
||||
scope_list.extend(read_scope_list)
|
||||
return {"{}!user={}".format(scope, name) for scope in scope_list}
|
||||
|
||||
|
||||
@@ -88,18 +86,18 @@ def _get_scope_hierarchy():
|
||||
|
||||
scopes = {
|
||||
'self': None,
|
||||
'all': None, # Optional 'read:all' as subscope, not implemented at this stage
|
||||
'users': ['read:users', 'users:activity', 'users:servers'],
|
||||
'all': None,
|
||||
'users': ['read:users', 'users:groups', 'users:activity'],
|
||||
'read:users': [
|
||||
'read:users:name',
|
||||
'read:users:groups',
|
||||
'read:users:activity',
|
||||
'read:users:servers',
|
||||
],
|
||||
'users:tokens': ['read:users:tokens'],
|
||||
'admin:users': ['admin:users:auth_state'],
|
||||
'admin:users:servers': ['admin:users:server_state'],
|
||||
'groups': ['read:groups'],
|
||||
'users:servers': ['read:users:servers'],
|
||||
'admin:groups': None,
|
||||
'read:services': None,
|
||||
'read:hub': None,
|
||||
@@ -113,13 +111,21 @@ def _get_scope_hierarchy():
|
||||
def horizontal_filter(func):
|
||||
"""Decorator to account for horizontal filtering in scope syntax"""
|
||||
|
||||
def expand_server_filter(hor_filter):
|
||||
resource, mark, value = hor_filter.partition('=')
|
||||
if resource == 'server':
|
||||
user, mark, server = value.partition('/')
|
||||
return f'read:users:name!user={user}'
|
||||
|
||||
def ignore(scopename):
|
||||
# temporarily remove horizontal filtering if present
|
||||
scopename, mark, hor_filter = scopename.partition('!')
|
||||
expanded_scope = func(scopename)
|
||||
# add the filter back
|
||||
full_expanded_scope = {scope + mark + hor_filter for scope in expanded_scope}
|
||||
|
||||
server_filter = expand_server_filter(hor_filter)
|
||||
if server_filter:
|
||||
full_expanded_scope.add(server_filter)
|
||||
return full_expanded_scope
|
||||
|
||||
return ignore
|
||||
|
@@ -158,7 +158,7 @@ def fill_user(model):
|
||||
model.setdefault('pending', None)
|
||||
model.setdefault('created', TIMESTAMP)
|
||||
model.setdefault('last_activity', TIMESTAMP)
|
||||
model.setdefault('servers', {})
|
||||
# model.setdefault('servers', {})
|
||||
return model
|
||||
|
||||
|
||||
|
@@ -86,7 +86,7 @@ async def test_default_server(app, named_servers):
|
||||
|
||||
user_model = normalize_user(r.json())
|
||||
assert user_model == fill_user(
|
||||
{'name': username, 'roles': ['user'], 'servers': {}, 'auth_state': None}
|
||||
{'name': username, 'roles': ['user'], 'auth_state': None}
|
||||
)
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ async def test_delete_named_server(app, named_servers):
|
||||
|
||||
user_model = normalize_user(r.json())
|
||||
assert user_model == fill_user(
|
||||
{'name': username, 'roles': ['user'], 'auth_state': None, 'servers': {}}
|
||||
{'name': username, 'roles': ['user'], 'auth_state': None}
|
||||
)
|
||||
# wrapper Spawner is gone
|
||||
assert servername not in user.spawners
|
||||
|
@@ -181,11 +181,10 @@ def test_orm_roles_delete_cascade(db):
|
||||
'users',
|
||||
'read:users',
|
||||
'users:activity',
|
||||
'users:servers',
|
||||
'users:groups',
|
||||
'read:users:name',
|
||||
'read:users:groups',
|
||||
'read:users:activity',
|
||||
'read:users:servers',
|
||||
},
|
||||
),
|
||||
(
|
||||
@@ -195,7 +194,6 @@ def test_orm_roles_delete_cascade(db):
|
||||
'read:users:name',
|
||||
'read:users:groups',
|
||||
'read:users:activity',
|
||||
'read:users:servers',
|
||||
},
|
||||
),
|
||||
(['read:users:servers'], {'read:users:servers'}),
|
||||
|
@@ -289,10 +289,11 @@ def create_user_with_scopes(app, create_temp_role):
|
||||
counter = 0
|
||||
get_role = create_temp_role
|
||||
|
||||
def temp_user_creator(*scopes):
|
||||
def temp_user_creator(*scopes, name=None):
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
name = f"temp_user_{counter}"
|
||||
if name is None:
|
||||
counter += 1
|
||||
name = f"temp_user_{counter}"
|
||||
role = get_role(scopes)
|
||||
orm_user = orm.User(name=name)
|
||||
app.db.add(orm_user)
|
||||
@@ -314,10 +315,11 @@ def create_service_with_scopes(app, create_temp_role):
|
||||
counter = 0
|
||||
role_function = create_temp_role
|
||||
|
||||
def temp_service_creator(*scopes):
|
||||
def temp_service_creator(*scopes, name=None):
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
name = f"temp_service_{counter}"
|
||||
if name is None:
|
||||
counter += 1
|
||||
name = f"temp_service_{counter}"
|
||||
role = role_function(scopes)
|
||||
app.services.append({'name': name})
|
||||
app.init_services()
|
||||
@@ -492,10 +494,10 @@ async def test_vertical_filter(app, create_user_with_scopes):
|
||||
|
||||
|
||||
async def test_stacked_vertical_filter(app, create_user_with_scopes):
|
||||
user = create_user_with_scopes('read:users:activity', 'read:users:servers')
|
||||
user = create_user_with_scopes('read:users:activity', 'read:users:groups')
|
||||
r = await api_request(app, 'users', headers=auth_header(app.db, user.name))
|
||||
assert r.status_code == 200
|
||||
allowed_keys = {'name', 'kind', 'servers', 'last_activity'}
|
||||
allowed_keys = {'name', 'kind', 'groups', 'last_activity'}
|
||||
result_model = set([key for user in r.json() for key in user.keys()])
|
||||
assert result_model == allowed_keys
|
||||
|
||||
@@ -561,42 +563,48 @@ async def test_metascope_all_expansion(app, create_user_with_scopes):
|
||||
"scopes, can_stop ,num_servers, keys_in, keys_out",
|
||||
[
|
||||
(['read:users:servers!user=almond'], False, 2, {'name'}, {'state'}),
|
||||
(['read:users:servers!group=nuts'], False, 2, {'name'}, {'state'}),
|
||||
(['admin:users', 'read:users'], False, 0, set(), set()),
|
||||
(
|
||||
['read:users:servers!group=nuts', 'users:servers'],
|
||||
True,
|
||||
2,
|
||||
{'name'},
|
||||
{'state'},
|
||||
),
|
||||
(
|
||||
['admin:users:server_state', 'read:users:servers'],
|
||||
True, # Todo: test for server stop
|
||||
False,
|
||||
2,
|
||||
{'name', 'state'},
|
||||
set(),
|
||||
),
|
||||
(['users:servers', 'read:users:name'], True, 0, set(), set()),
|
||||
(
|
||||
[
|
||||
'read:users:name!user=almond',
|
||||
'read:users:servers!server=almond/bianca',
|
||||
'admin:users:server_state!server=almond/bianca',
|
||||
],
|
||||
False,
|
||||
0, # fixme: server-scope not working yet
|
||||
1,
|
||||
{'name', 'state'},
|
||||
set(),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_server_state_access(
|
||||
app, scopes, can_stop, num_servers, keys_in, keys_out
|
||||
app,
|
||||
create_user_with_scopes,
|
||||
create_service_with_scopes,
|
||||
scopes,
|
||||
can_stop,
|
||||
num_servers,
|
||||
keys_in,
|
||||
keys_out,
|
||||
):
|
||||
with mock.patch.dict(
|
||||
app.tornado_settings,
|
||||
{'allow_named_servers': True, 'named_server_limit_per_user': 2},
|
||||
):
|
||||
## 1. Test a user can access all servers without auth_state
|
||||
## 2. Test a service with admin:user but no admin:users:servers gets no access to any server data
|
||||
## 3. Test a service with admin:user:server_state gets access to auth_state
|
||||
## 4. Test a service with user:servers!server=x gives access to one server, and the correct server.
|
||||
## 5. Test a service with users:servers!group=x gives access to both servers
|
||||
username = 'almond'
|
||||
user = add_user(app.db, app, name=username)
|
||||
user = create_user_with_scopes('self', name='almond')
|
||||
group_name = 'nuts'
|
||||
group = orm.Group.find(app.db, name=group_name)
|
||||
if not group:
|
||||
@@ -605,36 +613,38 @@ async def test_server_state_access(
|
||||
group.users.append(user)
|
||||
app.db.commit()
|
||||
server_names = ['bianca', 'terry']
|
||||
try:
|
||||
for server_name in server_names:
|
||||
await api_request(
|
||||
app, 'users', username, 'servers', server_name, method='post'
|
||||
)
|
||||
role = orm.Role(name=f"{username}-role", scopes=scopes)
|
||||
app.db.add(role)
|
||||
app.db.commit()
|
||||
service_name = 'server_accessor'
|
||||
service = orm.Service(name=service_name)
|
||||
app.db.add(service)
|
||||
service.roles.append(role)
|
||||
app.db.commit()
|
||||
api_token = service.new_api_token()
|
||||
await app.init_roles()
|
||||
headers = {'Authorization': 'token %s' % api_token}
|
||||
r = await api_request(app, 'users', username, headers=headers)
|
||||
r.raise_for_status()
|
||||
user_model = r.json()
|
||||
if num_servers:
|
||||
assert 'servers' in user_model
|
||||
server_models = user_model['servers']
|
||||
assert len(server_models) == num_servers
|
||||
for server, server_model in server_models.items():
|
||||
assert keys_in.issubset(server_model)
|
||||
assert keys_out.isdisjoint(server_model)
|
||||
else:
|
||||
assert 'servers' not in user_model
|
||||
finally:
|
||||
app.db.delete(role)
|
||||
app.db.delete(service)
|
||||
app.db.delete(group)
|
||||
app.db.commit()
|
||||
for server_name in server_names:
|
||||
await api_request(
|
||||
app, 'users', user.name, 'servers', server_name, method='post'
|
||||
)
|
||||
service = create_service_with_scopes(*scopes)
|
||||
api_token = service.new_api_token()
|
||||
await app.init_roles()
|
||||
headers = {'Authorization': 'token %s' % api_token}
|
||||
r = await api_request(app, 'users', user.name, headers=headers)
|
||||
r.raise_for_status()
|
||||
user_model = r.json()
|
||||
if num_servers:
|
||||
assert 'servers' in user_model
|
||||
server_models = user_model['servers']
|
||||
assert len(server_models) == num_servers
|
||||
for server, server_model in server_models.items():
|
||||
assert keys_in.issubset(server_model)
|
||||
assert keys_out.isdisjoint(server_model)
|
||||
else:
|
||||
assert 'servers' not in user_model
|
||||
r = await api_request(
|
||||
app,
|
||||
'users',
|
||||
user.name,
|
||||
'servers',
|
||||
server_names[0],
|
||||
method='delete',
|
||||
headers=headers,
|
||||
)
|
||||
if can_stop:
|
||||
assert r.status_code == 204
|
||||
else:
|
||||
assert r.status_code == 403
|
||||
app.db.delete(group)
|
||||
app.db.commit()
|
||||
|
@@ -287,19 +287,6 @@ def authenticated_403(self):
|
||||
raise web.HTTPError(403)
|
||||
|
||||
|
||||
@auth_decorator
|
||||
def admin_only(self):
|
||||
"""Decorator for restricting access to admin users
|
||||
Deprecated in favor of scopes.need_scope()
|
||||
"""
|
||||
user = self.current_user
|
||||
app_log.warning(
|
||||
"Admin decorator is deprecated and will be removed soon. Use scope-based decorator instead"
|
||||
)
|
||||
if user is None or not user.admin:
|
||||
raise web.HTTPError(403)
|
||||
|
||||
|
||||
@auth_decorator
|
||||
def metrics_authentication(self):
|
||||
"""Decorator for restricting access to metrics"""
|
||||
|
Reference in New Issue
Block a user