Add policy support
Using oslo.policy to support access control for Distil API access, which will follow the community way. Change-Id: I670d9fde4f5c368e82c26b512b7c1e46c2e380ec
This commit is contained in:
parent
6c69d161e9
commit
09b03931eb
53
distil/api/acl.py
Normal file
53
distil/api/acl.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Copyright (c) 2016 Catalyst IT Ltd.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Policy enforcer of Distil"""
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_policy import policy
|
||||||
|
|
||||||
|
from distil import context
|
||||||
|
from distil import exceptions
|
||||||
|
|
||||||
|
ENFORCER = None
|
||||||
|
|
||||||
|
|
||||||
|
def setup_policy():
|
||||||
|
global ENFORCER
|
||||||
|
ENFORCER = policy.Enforcer(cfg.CONF)
|
||||||
|
|
||||||
|
|
||||||
|
def check_is_admin(ctx):
|
||||||
|
credentials = ctx.to_dict()
|
||||||
|
target = credentials
|
||||||
|
return ENFORCER.enforce('context_is_admin', target, credentials)
|
||||||
|
|
||||||
|
|
||||||
|
def enforce(rule):
|
||||||
|
def decorator(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def handler(*args, **kwargs):
|
||||||
|
ctx = context.ctx()
|
||||||
|
ctx.is_admin = check_is_admin(ctx)
|
||||||
|
|
||||||
|
ENFORCER.enforce(rule, {}, ctx.to_dict(), do_raise=True,
|
||||||
|
exc=exceptions.Forbidden)
|
||||||
|
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return handler
|
||||||
|
|
||||||
|
return decorator
|
@ -17,6 +17,7 @@ import flask
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from distil.api import auth
|
from distil.api import auth
|
||||||
|
from distil.api import acl
|
||||||
from distil.api import v2 as api_v2
|
from distil.api import v2 as api_v2
|
||||||
from distil import config
|
from distil import config
|
||||||
from distil.utils import api
|
from distil.utils import api
|
||||||
@ -36,4 +37,5 @@ def make_app():
|
|||||||
|
|
||||||
app.register_blueprint(api_v2.rest, url_prefix="/v2")
|
app.register_blueprint(api_v2.rest, url_prefix="/v2")
|
||||||
app.wsgi_app = auth.wrap(app.wsgi_app, CONF)
|
app.wsgi_app = auth.wrap(app.wsgi_app, CONF)
|
||||||
|
acl.setup_policy()
|
||||||
return app
|
return app
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2014 Catalyst IT Ltd
|
# Copyright (c) 2016 Catalyst IT Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -18,6 +18,7 @@ from dateutil import parser
|
|||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from distil import exceptions
|
from distil import exceptions
|
||||||
|
from distil.api import acl
|
||||||
from distil.service.api.v2 import costs
|
from distil.service.api.v2 import costs
|
||||||
from distil.service.api.v2 import health
|
from distil.service.api.v2 import health
|
||||||
from distil.service.api.v2 import prices
|
from distil.service.api.v2 import prices
|
||||||
@ -28,6 +29,11 @@ LOG = log.getLogger(__name__)
|
|||||||
rest = api.Rest('v2', __name__)
|
rest = api.Rest('v2', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@rest.get('/health')
|
||||||
|
def health_get():
|
||||||
|
return api.render(health=health.get_health())
|
||||||
|
|
||||||
|
|
||||||
@rest.get('/prices')
|
@rest.get('/prices')
|
||||||
def prices_get():
|
def prices_get():
|
||||||
format = api.get_request_args().get('format', None)
|
format = api.get_request_args().get('format', None)
|
||||||
@ -44,6 +50,7 @@ def _get_usage_args():
|
|||||||
|
|
||||||
|
|
||||||
@rest.get('/costs')
|
@rest.get('/costs')
|
||||||
|
@acl.enforce("rating:costs:get")
|
||||||
def costs_get():
|
def costs_get():
|
||||||
project_id, start, end = _get_usage_args()
|
project_id, start, end = _get_usage_args()
|
||||||
try:
|
try:
|
||||||
@ -54,15 +61,11 @@ def costs_get():
|
|||||||
return api.render(status=400, error=str(e))
|
return api.render(status=400, error=str(e))
|
||||||
|
|
||||||
|
|
||||||
@rest.get('/usage')
|
@rest.get('/usages')
|
||||||
|
@acl.enforce("rating:usages:get")
|
||||||
def usage_get():
|
def usage_get():
|
||||||
project_id, start, end = _get_usage_args()
|
project_id, start, end = _get_usage_args()
|
||||||
try:
|
try:
|
||||||
return api.render(usage=costs.get_usage(project_id, start, end))
|
return api.render(usage=costs.get_usage(project_id, start, end))
|
||||||
except (exceptions.DateTimeException, exceptions.NotFoundException) as e:
|
except (exceptions.DateTimeException, exceptions.NotFoundException) as e:
|
||||||
return api.render(status=400, error=str(e))
|
return api.render(status=400, error=str(e))
|
||||||
|
|
||||||
|
|
||||||
@rest.get('/health')
|
|
||||||
def health_get():
|
|
||||||
return api.render(health=health.get_health())
|
|
||||||
|
@ -13,87 +13,53 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import eventlet
|
|
||||||
from eventlet.green import threading
|
from eventlet.green import threading
|
||||||
from eventlet.green import time
|
|
||||||
from eventlet import greenpool
|
|
||||||
from eventlet import semaphore
|
|
||||||
from oslo_config import cfg
|
|
||||||
|
|
||||||
from distil.api import acl
|
|
||||||
from distil import exceptions as ex
|
|
||||||
from distil.i18n import _
|
|
||||||
from distil.i18n import _LE
|
|
||||||
from distil.i18n import _LW
|
|
||||||
from oslo_context import context
|
from oslo_context import context
|
||||||
from oslo_log import log as logging
|
|
||||||
|
from distil import exceptions
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
class RequestContext(context.RequestContext):
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
def __init__(self, project_id=None, overwrite=True,
|
||||||
|
auth_token=None, user=None, tenant=None, domain=None,
|
||||||
|
user_domain=None, project_domain=None, is_admin=False,
|
||||||
|
read_only=False, show_deleted=False, request_id=None,
|
||||||
|
instance_uuid=None, roles=None, **kwargs):
|
||||||
|
super(RequestContext, self).__init__(auth_token=auth_token,
|
||||||
|
user=user,
|
||||||
|
tenant=tenant,
|
||||||
|
domain=domain,
|
||||||
|
user_domain=user_domain,
|
||||||
|
project_domain=project_domain,
|
||||||
|
is_admin=is_admin,
|
||||||
|
read_only=read_only,
|
||||||
|
show_deleted=False,
|
||||||
|
request_id=request_id,
|
||||||
|
roles=roles)
|
||||||
|
self.project_id = project_id or self.tenant
|
||||||
|
if overwrite or not hasattr(context._request_store, 'context'):
|
||||||
|
self.update_store()
|
||||||
|
|
||||||
|
def update_store(self):
|
||||||
|
context._request_store.context = self
|
||||||
|
|
||||||
|
|
||||||
class Context(context.RequestContext):
|
def make_context(*args, **kwargs):
|
||||||
def __init__(self,
|
return RequestContext(*args, **kwargs)
|
||||||
user_id=None,
|
|
||||||
tenant_id=None,
|
|
||||||
token=None,
|
|
||||||
service_catalog=None,
|
|
||||||
username=None,
|
|
||||||
tenant_name=None,
|
|
||||||
roles=None,
|
|
||||||
is_admin=None,
|
|
||||||
remote_semaphore=None,
|
|
||||||
auth_uri=None,
|
|
||||||
**kwargs):
|
|
||||||
if kwargs:
|
|
||||||
LOG.warn(_LW('Arguments dropped when creating context: %s'),
|
|
||||||
kwargs)
|
|
||||||
self.user_id = user_id
|
|
||||||
self.tenant_id = tenant_id
|
|
||||||
self.token = token
|
|
||||||
self.service_catalog = service_catalog
|
|
||||||
self.username = username
|
|
||||||
self.tenant_name = tenant_name
|
|
||||||
self.is_admin = is_admin
|
|
||||||
self.remote_semaphore = remote_semaphore or semaphore.Semaphore(
|
|
||||||
CONF.cluster_remote_threshold)
|
|
||||||
self.roles = roles
|
|
||||||
self.auth_uri = auth_uri
|
|
||||||
|
|
||||||
def clone(self):
|
|
||||||
return Context(
|
|
||||||
self.user_id,
|
|
||||||
self.tenant_id,
|
|
||||||
self.token,
|
|
||||||
self.service_catalog,
|
|
||||||
self.username,
|
|
||||||
self.tenant_name,
|
|
||||||
self.roles,
|
|
||||||
self.is_admin,
|
|
||||||
self.remote_semaphore,
|
|
||||||
self.auth_uri)
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
'user_id': self.user_id,
|
|
||||||
'tenant_id': self.tenant_id,
|
|
||||||
'token': self.token,
|
|
||||||
'service_catalog': self.service_catalog,
|
|
||||||
'username': self.username,
|
|
||||||
'tenant_name': self.tenant_name,
|
|
||||||
'is_admin': self.is_admin,
|
|
||||||
'roles': self.roles,
|
|
||||||
'auth_uri': self.auth_uri,
|
|
||||||
}
|
|
||||||
|
|
||||||
def is_auth_capable(self):
|
|
||||||
return (self.service_catalog and self.token and self.tenant_id and
|
|
||||||
self.user_id)
|
|
||||||
|
|
||||||
|
|
||||||
def get_admin_context():
|
def make_admin_context(show_deleted=False, all_tenants=False):
|
||||||
return Context(is_admin=True)
|
"""Create an administrator context.
|
||||||
|
|
||||||
|
:param show_deleted: if True, will show deleted items when query db
|
||||||
|
"""
|
||||||
|
context = RequestContext(user_id=None,
|
||||||
|
project=None,
|
||||||
|
is_admin=True,
|
||||||
|
show_deleted=show_deleted,
|
||||||
|
all_tenants=all_tenants)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
_CTX_STORE = threading.local()
|
_CTX_STORE = threading.local()
|
||||||
@ -106,7 +72,7 @@ def has_ctx():
|
|||||||
|
|
||||||
def ctx():
|
def ctx():
|
||||||
if not has_ctx():
|
if not has_ctx():
|
||||||
raise ex.IncorrectStateError(_("Context isn't available here"))
|
raise exceptions.IncorrectStateError(_("Context isn't available here"))
|
||||||
return getattr(_CTX_STORE, _CTX_KEY)
|
return getattr(_CTX_STORE, _CTX_KEY)
|
||||||
|
|
||||||
|
|
||||||
@ -117,6 +83,9 @@ def current():
|
|||||||
def set_ctx(new_ctx):
|
def set_ctx(new_ctx):
|
||||||
if not new_ctx and has_ctx():
|
if not new_ctx and has_ctx():
|
||||||
delattr(_CTX_STORE, _CTX_KEY)
|
delattr(_CTX_STORE, _CTX_KEY)
|
||||||
|
if hasattr(context._request_store, 'context'):
|
||||||
|
delattr(context._request_store, 'context')
|
||||||
|
|
||||||
if new_ctx:
|
if new_ctx:
|
||||||
setattr(_CTX_STORE, _CTX_KEY, new_ctx)
|
setattr(_CTX_STORE, _CTX_KEY, new_ctx)
|
||||||
|
setattr(context._request_store, 'context', new_ctx)
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_utils import uuidutils
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from distil.i18n import _
|
from distil.i18n import _
|
||||||
@ -21,41 +22,23 @@ _FATAL_EXCEPTION_FORMAT_ERRORS = False
|
|||||||
|
|
||||||
|
|
||||||
class DistilException(Exception):
|
class DistilException(Exception):
|
||||||
"""Base Distil Exception
|
"""Base Exception for the project
|
||||||
|
|
||||||
To correctly use this class, inherit from it and define
|
To correctly use this class, inherit from it and define
|
||||||
a 'message' property. That message will get printf'd
|
a 'message' and 'code' properties.
|
||||||
with the keyword arguments provided to the constructor.
|
|
||||||
"""
|
"""
|
||||||
|
message = _("An unknown exception occurred")
|
||||||
|
code = "UNKNOWN_EXCEPTION"
|
||||||
|
|
||||||
msg_fmt = _("An unknown exception occurred.")
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
def __init__(self, message=None, **kwargs):
|
def __init__(self):
|
||||||
self.kwargs = kwargs
|
super(DistilException, self).__init__(
|
||||||
|
'%s: %s' % (self.code, self.message))
|
||||||
if 'code' not in self.kwargs:
|
self.uuid = uuidutils.generate_uuid()
|
||||||
try:
|
self.message = (_('%(message)s\nError ID: %(id)s')
|
||||||
self.kwargs['code'] = self.code
|
% {'message': self.message, 'id': self.uuid})
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not message:
|
|
||||||
try:
|
|
||||||
message = self.msg_fmt % kwargs
|
|
||||||
except KeyError:
|
|
||||||
exc_info = sys.exc_info()
|
|
||||||
if _FATAL_EXCEPTION_FORMAT_ERRORS:
|
|
||||||
raise exc_info[0], exc_info[1], exc_info[2]
|
|
||||||
else:
|
|
||||||
message = self.msg_fmt
|
|
||||||
|
|
||||||
super(DistilException, self).__init__(message)
|
|
||||||
|
|
||||||
def format_message(self):
|
|
||||||
if self.__class__.__name__.endswith('_Remote'):
|
|
||||||
return self.args[0]
|
|
||||||
else:
|
|
||||||
return unicode(self)
|
|
||||||
|
|
||||||
|
|
||||||
class IncorrectStateError(DistilException):
|
class IncorrectStateError(DistilException):
|
||||||
@ -97,5 +80,13 @@ class MalformedRequestBody(DistilException):
|
|||||||
|
|
||||||
|
|
||||||
class DateTimeException(DistilException):
|
class DateTimeException(DistilException):
|
||||||
# This message should be replaced when thrown to be more specific:
|
|
||||||
message = _("An unexpected date, date format, or date range was given.")
|
message = _("An unexpected date, date format, or date range was given.")
|
||||||
|
|
||||||
|
def __init__(self, message=None):
|
||||||
|
self.code = 400
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
class Forbidden(DistilException):
|
||||||
|
code = "FORBIDDEN"
|
||||||
|
message = _("You are not authorized to complete this action")
|
||||||
|
@ -39,9 +39,8 @@ def _validate_project_and_range(project_id, start, end):
|
|||||||
start = datetime.strptime(start, constants.iso_time)
|
start = datetime.strptime(start, constants.iso_time)
|
||||||
else:
|
else:
|
||||||
raise exceptions.DateTimeException(
|
raise exceptions.DateTimeException(
|
||||||
code=400,
|
|
||||||
message=(
|
message=(
|
||||||
"missing parameter:" +
|
"Missing parameter:" +
|
||||||
"'start' in format: y-m-d or y-m-dTH:M:S"))
|
"'start' in format: y-m-d or y-m-dTH:M:S"))
|
||||||
if not end:
|
if not end:
|
||||||
end = datetime.utcnow()
|
end = datetime.utcnow()
|
||||||
@ -52,15 +51,17 @@ def _validate_project_and_range(project_id, start, end):
|
|||||||
end = datetime.strptime(end, constants.iso_time)
|
end = datetime.strptime(end, constants.iso_time)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise exceptions.DateTimeException(
|
raise exceptions.DateTimeException(
|
||||||
code=400,
|
|
||||||
message=(
|
message=(
|
||||||
"missing parameter: " +
|
"Missing parameter: " +
|
||||||
"'end' in format: y-m-d or y-m-dTH:M:S"))
|
"'end' in format: y-m-d or y-m-dTH:M:S"))
|
||||||
|
|
||||||
if end <= start:
|
if end <= start:
|
||||||
raise exceptions.DateTimeException(
|
raise exceptions.DateTimeException(
|
||||||
code=400, message="End date must be greater than start.")
|
message="End date must be greater than start.")
|
||||||
|
|
||||||
|
if not project_id:
|
||||||
|
raise exceptions.NotFoundException(value='project_id',
|
||||||
|
message="Missing parameter: %s")
|
||||||
valid_project = db_api.project_get(project_id)
|
valid_project = db_api.project_get(project_id)
|
||||||
|
|
||||||
return valid_project, start, end
|
return valid_project, start, end
|
||||||
|
0
distil/tests/unit/api/__init__.py
Normal file
0
distil/tests/unit/api/__init__.py
Normal file
57
distil/tests/unit/api/test_acl.py
Normal file
57
distil/tests/unit/api/test_acl.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Copyright (c) 2014 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import mock
|
||||||
|
from oslo_policy import policy as cpolicy
|
||||||
|
|
||||||
|
from distil.api import acl
|
||||||
|
from distil import exceptions as ex
|
||||||
|
from distil.tests.unit import base
|
||||||
|
from distil import context
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestAcl(base.DistilTestCase):
|
||||||
|
|
||||||
|
def _set_policy(self, json):
|
||||||
|
acl.setup_policy()
|
||||||
|
rules = cpolicy.Rules.load_json(json)
|
||||||
|
acl.ENFORCER.set_rules(rules, use_conf=False)
|
||||||
|
|
||||||
|
def test_policy_allow(self):
|
||||||
|
@acl.enforce("rating:get_all")
|
||||||
|
def test():
|
||||||
|
pass
|
||||||
|
|
||||||
|
json = '{"rating:get_all": ""}'
|
||||||
|
self._set_policy(json)
|
||||||
|
|
||||||
|
test()
|
||||||
|
|
||||||
|
def test_policy_deny(self):
|
||||||
|
@acl.enforce("rating:get_all")
|
||||||
|
def test(context):
|
||||||
|
pass
|
||||||
|
|
||||||
|
json = '{"rating:get_all": "!"}'
|
||||||
|
self._set_policy(json)
|
||||||
|
|
||||||
|
self.assertRaises(ex.Forbidden, test, context.RequestContext())
|
||||||
|
|
||||||
|
@mock.patch('flask.Request')
|
||||||
|
def test_route_post(self, get_deta_mock):
|
||||||
|
get_deta_mock.return_value = '{"foo": "bar"}'
|
||||||
|
pass
|
46
distil/tests/unit/base.py
Normal file
46
distil/tests/unit/base.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import mock
|
||||||
|
from oslotest import base
|
||||||
|
|
||||||
|
from distil import context
|
||||||
|
|
||||||
|
|
||||||
|
class DistilTestCase(base.BaseTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DistilTestCase, self).setUp()
|
||||||
|
self.setup_context()
|
||||||
|
|
||||||
|
def setup_context(self, username="test_user", tenant_id="tenant_1",
|
||||||
|
auth_token="test_auth_token", tenant_name='test_tenant',
|
||||||
|
service_catalog=None, **kwargs):
|
||||||
|
self.addCleanup(context.set_ctx,
|
||||||
|
context.ctx() if context.has_ctx() else None)
|
||||||
|
|
||||||
|
context.set_ctx(context.RequestContext(
|
||||||
|
username=username, tenant_id=tenant_id,
|
||||||
|
auth_token=auth_token, service_catalog=service_catalog or {},
|
||||||
|
tenant_name=tenant_name, **kwargs))
|
||||||
|
|
||||||
|
class DistilWithDbTestCase(DistilTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(DistilWithDbTestCase, self).setUp()
|
||||||
|
|
||||||
|
self.override_config('connection', "sqlite://", group='database')
|
||||||
|
db_api.setup_db()
|
||||||
|
self.addCleanup(db_api.drop_db)
|
@ -21,6 +21,7 @@ from werkzeug import datastructures
|
|||||||
from distil import exceptions as ex
|
from distil import exceptions as ex
|
||||||
from distil.i18n import _
|
from distil.i18n import _
|
||||||
from distil.i18n import _LE
|
from distil.i18n import _LE
|
||||||
|
from distil import context
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from distil.utils import wsgi
|
from distil.utils import wsgi
|
||||||
|
|
||||||
@ -60,11 +61,23 @@ class Rest(flask.Blueprint):
|
|||||||
if status:
|
if status:
|
||||||
flask.request.status_code = status
|
flask.request.status_code = status
|
||||||
|
|
||||||
|
req_id = flask.request.headers.get('X-Openstack-Request-ID')
|
||||||
|
ctx = context.RequestContext(
|
||||||
|
user=flask.request.headers.get('X-User-Id'),
|
||||||
|
tenant=flask.request.headers.get('X-Tenant-Id'),
|
||||||
|
auth_token=flask.request.headers.get('X-Auth-Token'),
|
||||||
|
request_id=req_id,
|
||||||
|
roles=flask.request.headers.get('X-Roles', '').split(','))
|
||||||
|
|
||||||
|
context.set_ctx(ctx)
|
||||||
|
|
||||||
if flask.request.method in ['POST', 'PUT']:
|
if flask.request.method in ['POST', 'PUT']:
|
||||||
kwargs['data'] = request_data()
|
kwargs['data'] = request_data()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return func(**kwargs)
|
return func(**kwargs)
|
||||||
|
except ex.Forbidden as e:
|
||||||
|
return access_denied(e)
|
||||||
except ex.DistilException as e:
|
except ex.DistilException as e:
|
||||||
return bad_request(e)
|
return bad_request(e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -73,7 +86,7 @@ class Rest(flask.Blueprint):
|
|||||||
f_rule = rule
|
f_rule = rule
|
||||||
self.add_url_rule(f_rule, endpoint, handler, **options)
|
self.add_url_rule(f_rule, endpoint, handler, **options)
|
||||||
self.add_url_rule(f_rule + '.json', endpoint, handler, **options)
|
self.add_url_rule(f_rule + '.json', endpoint, handler, **options)
|
||||||
self.add_url_rule(f_rule + '.xml', endpoint, handler, **options)
|
|
||||||
return func
|
return func
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
@ -220,6 +233,18 @@ def bad_request(error):
|
|||||||
return render_error_message(error_code, error.message, error.code)
|
return render_error_message(error_code, error.message, error.code)
|
||||||
|
|
||||||
|
|
||||||
|
def access_denied(error):
|
||||||
|
error_code = 403
|
||||||
|
|
||||||
|
LOG.error(_LE("Access Denied: "
|
||||||
|
"error_code={code}, error_message={message}, "
|
||||||
|
"error_name={name}").format(code=error_code,
|
||||||
|
message=error.message,
|
||||||
|
name=error.code))
|
||||||
|
|
||||||
|
return render_error_message(error_code, error.message, error.code)
|
||||||
|
|
||||||
|
|
||||||
def not_found(error):
|
def not_found(error):
|
||||||
error_code = 404
|
error_code = 404
|
||||||
|
|
||||||
@ -227,4 +252,4 @@ def not_found(error):
|
|||||||
"error_code=%s, error_message=%s, error_name=%s",
|
"error_code=%s, error_message=%s, error_name=%s",
|
||||||
error_code, error.message, error.code)
|
error_code, error.message, error.code)
|
||||||
|
|
||||||
return render_error_message(error_code, error.message, error.code)
|
return render_error_message(error_code, error.message, error.code)
|
||||||
|
8
etc/policy.json.sample
Normal file
8
etc/policy.json.sample
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"context_is_admin": "role:admin",
|
||||||
|
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
|
||||||
|
"default": "rule:admin_or_owner",
|
||||||
|
|
||||||
|
"rating:costs:get": "rule:context_is_admin",
|
||||||
|
"rating:usages:get": "rule:context_is_admin",
|
||||||
|
}
|
@ -24,6 +24,7 @@ oslo.context>=2.2.0 # Apache-2.0
|
|||||||
oslo.db>=4.1.0 # Apache-2.0
|
oslo.db>=4.1.0 # Apache-2.0
|
||||||
oslo.i18n>=2.1.0 # Apache-2.0
|
oslo.i18n>=2.1.0 # Apache-2.0
|
||||||
oslo.log>=1.14.0 # Apache-2.0
|
oslo.log>=1.14.0 # Apache-2.0
|
||||||
|
oslo.policy>=1.14.0 # Apache-2.0
|
||||||
oslo.serialization>=1.10.0 # Apache-2.0
|
oslo.serialization>=1.10.0 # Apache-2.0
|
||||||
oslo.service>=1.0.0 # Apache-2.0
|
oslo.service>=1.0.0 # Apache-2.0
|
||||||
oslo.utils>=3.5.0 # Apache-2.0
|
oslo.utils>=3.5.0 # Apache-2.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user