Check policy when handling a HTTP request
* Add default policy for handling the create request. * Allow it to be accessed only by nova service. * Remove unused code copied from cinder. Change-Id: Ieaa407f27c6774d1fd17850a9571de5554360bae
This commit is contained in:
parent
b3f961e331
commit
462305315c
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
build
|
||||
files/join.conf
|
||||
files/policy.yaml.sample
|
||||
tools/lintstack.head.py
|
||||
*.pyc
|
||||
*.egg-info/
|
||||
|
3
files/policy-generator.conf
Normal file
3
files/policy-generator.conf
Normal file
@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
output_file = files/policy.yaml.sample
|
||||
namespace = novajoin
|
@ -22,8 +22,6 @@ import copy
|
||||
from oslo_config import cfg
|
||||
from oslo_context import context
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import timeutils
|
||||
import six
|
||||
|
||||
from novajoin import policy
|
||||
|
||||
@ -39,54 +37,9 @@ class RequestContext(context.RequestContext):
|
||||
Represents the user taking a given action within the system.
|
||||
|
||||
"""
|
||||
def __init__(self, user_id, project_id, is_admin=None, read_deleted="no",
|
||||
roles=None, project_name=None, remote_address=None,
|
||||
timestamp=None, request_id=None, auth_token=None,
|
||||
overwrite=True, quota_class=None, service_catalog=None,
|
||||
domain=None, user_domain=None, project_domain=None,
|
||||
**kwargs):
|
||||
"""Initialize RequestContext.
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
:param read_deleted: 'no' indicates deleted records are hidden, 'yes'
|
||||
indicates deleted records are visible, 'only' indicates that
|
||||
*only* deleted records are visible.
|
||||
|
||||
:param overwrite: Set to False to ensure that the greenthread local
|
||||
copy of the index is not overwritten.
|
||||
|
||||
:param kwargs: Extra arguments that might be present, but we ignore
|
||||
because they possibly came in from older rpc messages.
|
||||
"""
|
||||
|
||||
super(RequestContext, self).__init__(auth_token=auth_token,
|
||||
user=user_id,
|
||||
tenant=project_id,
|
||||
domain=domain,
|
||||
user_domain=user_domain,
|
||||
project_domain=project_domain,
|
||||
is_admin=is_admin,
|
||||
request_id=request_id,
|
||||
overwrite=overwrite,
|
||||
roles=roles)
|
||||
self.project_name = project_name
|
||||
self.read_deleted = read_deleted
|
||||
self.remote_address = remote_address
|
||||
if not timestamp:
|
||||
timestamp = timeutils.utcnow()
|
||||
elif isinstance(timestamp, six.string_types):
|
||||
timestamp = timeutils.parse_isotime(timestamp)
|
||||
self.timestamp = timestamp
|
||||
self.quota_class = quota_class
|
||||
|
||||
if service_catalog:
|
||||
# Only include required parts of service_catalog
|
||||
self.service_catalog = [s for s in service_catalog
|
||||
if s.get('type') in
|
||||
('identity', 'compute', 'object-store',
|
||||
'image')]
|
||||
else:
|
||||
# if list is empty or none
|
||||
self.service_catalog = []
|
||||
super(RequestContext, self).__init__(*args, **kwargs)
|
||||
|
||||
# We need to have RequestContext attributes defined
|
||||
# when policy.check_is_admin invokes request logging
|
||||
@ -96,39 +49,14 @@ class RequestContext(context.RequestContext):
|
||||
elif self.is_admin and 'admin' not in self.roles:
|
||||
self.roles.append('admin')
|
||||
|
||||
def _get_read_deleted(self):
|
||||
return self._read_deleted
|
||||
|
||||
def _set_read_deleted(self, read_deleted):
|
||||
if read_deleted not in ('no', 'yes', 'only'):
|
||||
raise ValueError("read_deleted can only be one of 'no', "
|
||||
"'yes' or 'only', not %r") % read_deleted
|
||||
self._read_deleted = read_deleted
|
||||
|
||||
def _del_read_deleted(self):
|
||||
del self._read_deleted
|
||||
|
||||
read_deleted = property(_get_read_deleted, _set_read_deleted,
|
||||
_del_read_deleted)
|
||||
|
||||
def to_dict(self):
|
||||
result = super(RequestContext, self).to_dict()
|
||||
result['user_id'] = self.user_id
|
||||
result['user_name'] = self.user_name
|
||||
result['project_id'] = self.project_id
|
||||
result['project_name'] = self.project_name
|
||||
result['domain'] = self.domain
|
||||
result['read_deleted'] = self.read_deleted
|
||||
result['remote_address'] = self.remote_address
|
||||
result['timestamp'] = self.timestamp.isoformat()
|
||||
result['quota_class'] = self.quota_class
|
||||
result['service_catalog'] = self.service_catalog
|
||||
result['request_id'] = self.request_id
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, values):
|
||||
return cls(**values)
|
||||
|
||||
def elevated(self, read_deleted=None, overwrite=False):
|
||||
"""Return a version of this context with admin flag set."""
|
||||
context = self.deepcopy()
|
||||
@ -144,25 +72,3 @@ class RequestContext(context.RequestContext):
|
||||
|
||||
def deepcopy(self):
|
||||
return copy.deepcopy(self)
|
||||
|
||||
# NOTE(sirp): the openstack/common version of RequestContext uses
|
||||
# tenant/user whereas the Cinder version uses project_id/user_id.
|
||||
# NOTE(adrienverge): The Cinder version of RequestContext now uses
|
||||
# tenant/user internally, so it is compatible with context-aware code from
|
||||
# openstack/common. We still need this shim for the rest of Cinder's
|
||||
# code.
|
||||
@property
|
||||
def project_id(self):
|
||||
return self.tenant
|
||||
|
||||
@project_id.setter
|
||||
def project_id(self, value):
|
||||
self.tenant = value
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
return self.user
|
||||
|
||||
@user_id.setter
|
||||
def user_id(self, value):
|
||||
self.user = value
|
||||
|
@ -131,10 +131,6 @@ class GlanceConnectionFailed(JoinException):
|
||||
message = "Connection to glance failed: %(reason)s"
|
||||
|
||||
|
||||
class ImageLimitExceeded(JoinException):
|
||||
message = "Image quota exceeded"
|
||||
|
||||
|
||||
class ImageNotAuthorized(JoinException):
|
||||
message = "Not authorized for image %(image_id)s."
|
||||
|
||||
|
@ -25,6 +25,7 @@ from novajoin.glance import get_default_image_service
|
||||
from novajoin.ipa import IPAClient
|
||||
from novajoin import keystone_client
|
||||
from novajoin.nova import get_instance
|
||||
from novajoin import policy
|
||||
from novajoin import util
|
||||
|
||||
|
||||
@ -112,6 +113,12 @@ class JoinController(Controller):
|
||||
LOG.error('No body in create request')
|
||||
raise base.Fault(webob.exc.HTTPBadRequest())
|
||||
|
||||
context = req.environ.get('novajoin.context')
|
||||
try:
|
||||
policy.authorize_action(context, 'join:create')
|
||||
except exception.PolicyNotAuthorized:
|
||||
raise base.Fault(webob.exc.HTTPForbidden())
|
||||
|
||||
instance_id = body.get('instance-id')
|
||||
image_id = body.get('image-id')
|
||||
project_id = body.get('project-id')
|
||||
@ -139,7 +146,6 @@ class JoinController(Controller):
|
||||
if enroll.lower() != 'true':
|
||||
LOG.debug('IPA enrollment not requested in instance creation')
|
||||
|
||||
context = req.environ.get('novajoin.context')
|
||||
image_service = get_default_image_service()
|
||||
image_metadata = {}
|
||||
try:
|
||||
|
@ -19,8 +19,6 @@ Simplified Common Auth Middleware from cinder.
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_middleware import request_id
|
||||
from oslo_serialization import jsonutils
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
@ -53,48 +51,10 @@ class JoinKeystoneContext(novajoin.base.Middleware):
|
||||
|
||||
@webob.dec.wsgify(RequestClass=novajoin.base.Request)
|
||||
def __call__(self, req):
|
||||
user_id = req.headers.get('X_USER')
|
||||
user_id = req.headers.get('X_USER_ID', user_id)
|
||||
if user_id is None:
|
||||
LOG.debug("Neither X_USER_ID nor X_USER found in request")
|
||||
return webob.exc.HTTPUnauthorized()
|
||||
# get the roles
|
||||
roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')]
|
||||
if 'X_TENANT_ID' in req.headers:
|
||||
# This is the new header since Keystone went to ID/Name
|
||||
project_id = req.headers['X_TENANT_ID']
|
||||
else:
|
||||
# This is for legacy compatibility
|
||||
project_id = req.headers['X_TENANT']
|
||||
|
||||
project_name = req.headers.get('X_TENANT_NAME')
|
||||
|
||||
req_id = req.environ.get(request_id.ENV_REQUEST_ID)
|
||||
|
||||
# Get the auth token
|
||||
auth_token = req.headers.get('X_AUTH_TOKEN',
|
||||
req.headers.get('X_STORAGE_TOKEN'))
|
||||
|
||||
# Build a context, including the auth_token...
|
||||
remote_address = req.remote_addr
|
||||
|
||||
service_catalog = None
|
||||
if req.headers.get('X_SERVICE_CATALOG') is not None:
|
||||
try:
|
||||
catalog_header = req.headers.get('X_SERVICE_CATALOG')
|
||||
service_catalog = jsonutils.loads(catalog_header)
|
||||
except ValueError:
|
||||
raise webob.exc.HTTPInternalServerError(
|
||||
explanation='Invalid service catalog json.')
|
||||
|
||||
ctx = context.RequestContext(user_id,
|
||||
project_id,
|
||||
project_name=project_name,
|
||||
roles=roles,
|
||||
auth_token=auth_token,
|
||||
remote_address=remote_address,
|
||||
service_catalog=service_catalog,
|
||||
request_id=req_id)
|
||||
|
||||
ctx = context.RequestContext.from_environ(req.environ)
|
||||
except KeyError:
|
||||
LOG.debug("Keystone middleware headers not found in request!")
|
||||
return webob.exc.HTTPUnauthorized()
|
||||
req.environ['novajoin.context'] = ctx
|
||||
return self.application
|
||||
|
@ -16,55 +16,78 @@
|
||||
"""Policy Engine"""
|
||||
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_policy import opts as policy_opts
|
||||
from oslo_policy import policy
|
||||
|
||||
from novajoin import config
|
||||
from novajoin import exception
|
||||
|
||||
CONF = cfg.CONF
|
||||
policy_opts.set_defaults(cfg.CONF, 'policy.json')
|
||||
CONF = config.CONF
|
||||
policy_opts.set_defaults(CONF, 'policy.json')
|
||||
|
||||
_ENFORCER = None
|
||||
|
||||
# We have only one endpoint, so there is not a lot of default rules
|
||||
_RULES = [
|
||||
policy.RuleDefault(
|
||||
'context_is_admin', 'role:admin',
|
||||
"Decides what is required for the 'is_admin:True' check to succeed."),
|
||||
policy.RuleDefault(
|
||||
'service_role', 'role:service',
|
||||
"service role"),
|
||||
policy.RuleDefault(
|
||||
'compute_service_user', 'user_name:nova and rule:service_role',
|
||||
"This is usualy the nova service user, which calls the novajoin API, "
|
||||
"configured in [vendordata_dynamic_auth] in nova.conf."),
|
||||
policy.DocumentedRuleDefault(
|
||||
'join:create', 'rule:compute_service_user',
|
||||
'Generate the OTP, register it with IPA',
|
||||
[{'path': '/', 'method': 'POST'}]
|
||||
)
|
||||
]
|
||||
|
||||
def init():
|
||||
|
||||
def list_rules():
|
||||
return _RULES
|
||||
|
||||
|
||||
def get_enforcer():
|
||||
global _ENFORCER # pylint: disable=global-statement
|
||||
if not _ENFORCER:
|
||||
_ENFORCER = policy.Enforcer(CONF)
|
||||
_ENFORCER.register_defaults(list_rules())
|
||||
return _ENFORCER
|
||||
|
||||
|
||||
def enforce_action(context, action):
|
||||
def authorize_action(context, action):
|
||||
"""Checks that the action can be done by the given context.
|
||||
|
||||
Applies a check to ensure the context's project_id and user_id can be
|
||||
applied to the given action using the policy enforcement api.
|
||||
Applies a check to ensure the context's project_id, user_id and others
|
||||
can be applied to the given action using the policy enforcement api.
|
||||
"""
|
||||
|
||||
return enforce(context, action, {'project_id': context.project_id,
|
||||
'user_id': context.user_id})
|
||||
return authorize(context, action, context.to_dict())
|
||||
|
||||
|
||||
def enforce(context, action, target):
|
||||
def authorize(context, action, target):
|
||||
"""Verifies that the action is valid on the target in this context.
|
||||
|
||||
:param context: cinder context
|
||||
:param context: novajoin context
|
||||
:param action: string representing the action to be checked
|
||||
this should be colon separated for clarity.
|
||||
i.e. ``compute:create_instance``,
|
||||
``compute:attach_volume``,
|
||||
``volume:attach_volume``
|
||||
|
||||
:param object: dictionary representing the object of the action
|
||||
:param target: dictionary representing the object of the action
|
||||
for object creation this should be a dictionary representing the
|
||||
location of the object e.g. ``{'project_id': context.project_id}``
|
||||
|
||||
:raises PolicyNotAuthorized: if verification fails.
|
||||
|
||||
"""
|
||||
init()
|
||||
|
||||
return _ENFORCER.enforce(action, target, context.to_dict(),
|
||||
return get_enforcer().authorize(action, target, context.to_dict(),
|
||||
do_raise=True,
|
||||
exc=exception.PolicyNotAuthorized,
|
||||
action=action)
|
||||
@ -76,7 +99,6 @@ def check_is_admin(roles, context=None):
|
||||
Can use roles or user_id from context to determine if user is admin.
|
||||
In a multi-domain configuration, roles alone may not be sufficient.
|
||||
"""
|
||||
init()
|
||||
|
||||
# include project_id on target to avoid KeyError if context_is_admin
|
||||
# policy definition is missing, and default admin_or_owner rule
|
||||
@ -90,4 +112,4 @@ def check_is_admin(roles, context=None):
|
||||
'user_id': context.user_id
|
||||
}
|
||||
|
||||
return _ENFORCER.enforce('context_is_admin', target, credentials)
|
||||
return get_enforcer().authorize('context_is_admin', target, credentials)
|
||||
|
@ -34,11 +34,22 @@ class HTTPRequest(webob.Request):
|
||||
if 'v1' in args[0]:
|
||||
kwargs['base_url'] = 'http://localhost/v1'
|
||||
use_admin_context = kwargs.pop('use_admin_context', False)
|
||||
use_nova_service_context = kwargs.pop('use_nova_context', True)
|
||||
version = kwargs.pop('version', '1.0')
|
||||
out = base.Request.blank(*args, **kwargs)
|
||||
out.environ['cinder.context'] = FakeRequestContext(
|
||||
fake.USER_ID,
|
||||
fake.PROJECT_ID,
|
||||
if use_nova_service_context:
|
||||
out.environ['novajoin.context'] = FakeRequestContext(
|
||||
user_id=fake.USER_ID,
|
||||
user_name='nova',
|
||||
roles=['service'],
|
||||
project_id=fake.PROJECT_ID,
|
||||
is_admin=use_admin_context)
|
||||
else:
|
||||
out.environ['novajoin.context'] = FakeRequestContext(
|
||||
user_id=fake.USER_ID,
|
||||
user_name='not_nova',
|
||||
roles=['not_service'],
|
||||
project_id=fake.PROJECT_ID,
|
||||
is_admin=use_admin_context)
|
||||
out.api_version_request = Join(version)
|
||||
return out
|
||||
|
@ -18,6 +18,7 @@ from oslo_serialization import jsonutils
|
||||
from testtools.matchers import MatchesRegex
|
||||
|
||||
from novajoin.base import Fault
|
||||
from novajoin import config
|
||||
from novajoin import join
|
||||
from novajoin import test
|
||||
from novajoin.tests.unit.api import fakes
|
||||
@ -36,6 +37,7 @@ class JoinTest(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.join_controller = join.JoinController()
|
||||
config.CONF([])
|
||||
super(JoinTest, self).setUp()
|
||||
|
||||
def test_no_body(self):
|
||||
@ -53,6 +55,21 @@ class JoinTest(test.TestCase):
|
||||
else:
|
||||
assert(False)
|
||||
|
||||
def test_unauthorized(self):
|
||||
body = {'test': 'test'}
|
||||
req = fakes.HTTPRequest.blank('/v1/', use_nova_context=False)
|
||||
req.method = 'POST'
|
||||
req.content_type = "application/json"
|
||||
|
||||
# Not using assertRaises because the exception is wrapped as
|
||||
# a Fault
|
||||
try:
|
||||
self.join_controller.create(req, body)
|
||||
except Fault as fault:
|
||||
assert fault.status_int == 403
|
||||
else:
|
||||
assert(False)
|
||||
|
||||
def test_no_instanceid(self):
|
||||
body = {"metadata": {"ipa_enroll": "True"},
|
||||
"image-id": fake.IMAGE_ID,
|
||||
|
@ -73,3 +73,9 @@ oslo.config.opts.defaults =
|
||||
console_scripts =
|
||||
novajoin-server = novajoin.wsgi:main
|
||||
novajoin-notify = novajoin.notifications:main
|
||||
|
||||
oslo.policy.policies =
|
||||
novajoin = novajoin.policy:list_rules
|
||||
|
||||
oslo.policy.enforcer =
|
||||
novajoin = novajoin.policy:get_enforcer
|
||||
|
6
tox.ini
6
tox.ini
@ -65,6 +65,7 @@ commands = sphinx-build -W -b html doc/source doc/build/html
|
||||
|
||||
[testenv:genconfig]
|
||||
sitepackages = False
|
||||
basepython = python3
|
||||
envdir = {toxworkdir}/pep8
|
||||
commands = oslo-config-generator --config-file=files/novajoin-config-generator.conf
|
||||
|
||||
@ -86,3 +87,8 @@ setenv =
|
||||
commands =
|
||||
/usr/bin/find . -type f -name "*.py[c|o]" -delete
|
||||
stestr run --slowest {posargs}
|
||||
|
||||
[testenv:genpolicy]
|
||||
basepython = python3
|
||||
envdir = {toxworkdir}/pep8
|
||||
commands = oslopolicy-sample-generator --config-file files/policy-generator.conf
|
||||
|
Loading…
Reference in New Issue
Block a user