Merge "Verify project-id when setting quota"
This commit is contained in:
commit
d7f1400c91
@ -68,6 +68,9 @@ api_v2_opts = [
|
||||
'means show all results by default'),
|
||||
cfg.IntOpt('max-limit-v2', default=1000,
|
||||
help='Max per-page limit for the V2 API'),
|
||||
cfg.BoolOpt('quotas-verify-project-id', default=False,
|
||||
help='Verify that the requested Project ID for quota target '
|
||||
'is a valid project in Keystone.'),
|
||||
]
|
||||
|
||||
api_admin_opts = [
|
||||
|
@ -14,9 +14,11 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import pecan
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate.api.v2.controllers import rest
|
||||
from designate.common import keystone
|
||||
from designate.objects.adapters import DesignateAdapter
|
||||
from designate.objects import QuotaList
|
||||
|
||||
@ -52,6 +54,13 @@ class QuotasController(rest.RestController):
|
||||
context = request.environ['context']
|
||||
body = request.body_dict
|
||||
|
||||
# NOTE(pas-ha) attempting to verify the validity of the project-id
|
||||
# on a best effort basis
|
||||
# this will raise only if KeystoneV3 endpoint is not found at all,
|
||||
# or the creds are passing but the project is not found
|
||||
if cfg.CONF['service:api'].quotas_verify_project_id:
|
||||
keystone.verify_project_id(context, tenant_id)
|
||||
|
||||
quotas = DesignateAdapter.parse('API_v2', body, QuotaList())
|
||||
|
||||
for quota in quotas:
|
||||
|
@ -19,6 +19,7 @@ from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_reports import guru_meditation_report as gmr
|
||||
|
||||
from designate.common import keystone
|
||||
from designate import hookpoints
|
||||
from designate import service
|
||||
from designate import utils
|
||||
@ -30,6 +31,7 @@ CONF = cfg.CONF
|
||||
CONF.import_opt('workers', 'designate.api', group='service:api')
|
||||
CONF.import_opt('threads', 'designate.api', group='service:api')
|
||||
cfg.CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token')
|
||||
keystone.register_keystone_opts(CONF)
|
||||
|
||||
|
||||
def main():
|
||||
|
95
designate/common/keystone.py
Normal file
95
designate/common/keystone.py
Normal file
@ -0,0 +1,95 @@
|
||||
#
|
||||
# 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.
|
||||
|
||||
from keystoneauth1 import exceptions as kse
|
||||
from keystoneauth1 import loading as ksa_loading
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate import exceptions
|
||||
from designate.i18n import _
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
keystone_group = cfg.OptGroup(
|
||||
name='keystone', title='Access to Keystone API')
|
||||
|
||||
|
||||
def register_keystone_opts(conf):
|
||||
conf.register_group(keystone_group)
|
||||
ksa_loading.register_adapter_conf_options(conf, keystone_group)
|
||||
ksa_loading.register_session_conf_options(conf, keystone_group)
|
||||
conf.set_default('service_type', 'identity', group=keystone_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
opts = ksa_loading.get_adapter_conf_options()
|
||||
opts.extend(ksa_loading.get_session_conf_options())
|
||||
return opts
|
||||
|
||||
|
||||
def verify_project_id(context, project_id):
|
||||
"""verify that a project_id exists.
|
||||
|
||||
This attempts to verify that a project id exists. If it does not,
|
||||
an HTTPBadRequest is emitted.
|
||||
|
||||
"""
|
||||
session = ksa_loading.load_session_from_conf_options(
|
||||
CONF, 'keystone', auth=context.get_auth_plugin())
|
||||
adap = ksa_loading.load_adapter_from_conf_options(
|
||||
CONF, 'keystone',
|
||||
session=session, min_version=(3, 0), max_version=(3, 'latest'))
|
||||
try:
|
||||
resp = adap.get('/projects/%s' % project_id, raise_exc=False)
|
||||
except kse.EndpointNotFound:
|
||||
LOG.error(
|
||||
"Keystone identity service version 3.0 was not found. This might "
|
||||
"be because your endpoint points to the v2.0 versioned endpoint "
|
||||
"which is not supported. Please fix this.")
|
||||
raise exceptions.KeystoneCommunicationFailure(
|
||||
_("KeystoneV3 endpoint not found"))
|
||||
except kse.ClientException:
|
||||
# something is wrong, like there isn't a keystone v3 endpoint,
|
||||
# or nova isn't configured for the interface to talk to it;
|
||||
# we'll take the pass and default to everything being ok.
|
||||
LOG.info("Unable to contact keystone to verify project_id")
|
||||
return True
|
||||
|
||||
if resp:
|
||||
# All is good with this 20x status
|
||||
return True
|
||||
elif resp.status_code == 404:
|
||||
# we got access, and we know this project is not there
|
||||
raise exceptions.InvalidProject(
|
||||
_("%s is not a valid project ID.") % project_id)
|
||||
|
||||
elif resp.status_code == 403:
|
||||
# we don't have enough permission to verify this, so default
|
||||
# to "it's ok".
|
||||
LOG.info(
|
||||
"Insufficient permissions for user %(user)s to verify "
|
||||
"existence of project_id %(pid)s",
|
||||
{"user": context.user_id, "pid": project_id})
|
||||
return True
|
||||
else:
|
||||
LOG.warning(
|
||||
"Unexpected response from keystone trying to "
|
||||
"verify project_id %(pid)s - resp: %(code)s %(content)s",
|
||||
{"pid": project_id,
|
||||
"code": resp.status_code,
|
||||
"content": resp.content})
|
||||
# realize we did something wrong, but move on with a warning
|
||||
return True
|
@ -16,6 +16,8 @@
|
||||
import itertools
|
||||
import copy
|
||||
|
||||
from keystoneauth1.access import service_catalog as ksa_service_catalog
|
||||
from keystoneauth1 import plugin
|
||||
from oslo_context import context
|
||||
from oslo_log import log as logging
|
||||
|
||||
@ -40,10 +42,11 @@ class DesignateContext(context.RequestContext):
|
||||
def __init__(self, service_catalog=None, all_tenants=False, abandon=None,
|
||||
tsigkey_id=None, original_tenant=None,
|
||||
edit_managed_records=False, hide_counts=False,
|
||||
client_addr=None, **kwargs):
|
||||
client_addr=None, user_auth_plugin=None, **kwargs):
|
||||
|
||||
super(DesignateContext, self).__init__(**kwargs)
|
||||
|
||||
self.user_auth_plugin = user_auth_plugin
|
||||
self.service_catalog = service_catalog
|
||||
self.tsigkey_id = tsigkey_id
|
||||
|
||||
@ -193,6 +196,49 @@ class DesignateContext(context.RequestContext):
|
||||
def client_addr(self, value):
|
||||
self._client_addr = value
|
||||
|
||||
def get_auth_plugin(self):
|
||||
if self.user_auth_plugin:
|
||||
return self.user_auth_plugin
|
||||
else:
|
||||
return _ContextAuthPlugin(self.auth_token, self.service_catalog)
|
||||
|
||||
|
||||
class _ContextAuthPlugin(plugin.BaseAuthPlugin):
|
||||
"""A keystoneauth auth plugin that uses the values from the Context.
|
||||
Ideally we would use the plugin provided by auth_token middleware however
|
||||
this plugin isn't serialized yet so we construct one from the serialized
|
||||
auth data.
|
||||
"""
|
||||
def __init__(self, auth_token, sc):
|
||||
super(_ContextAuthPlugin, self).__init__()
|
||||
|
||||
self.auth_token = auth_token
|
||||
self.service_catalog = ksa_service_catalog.ServiceCatalogV2(sc)
|
||||
|
||||
def get_token(self, *args, **kwargs):
|
||||
return self.auth_token
|
||||
|
||||
def get_endpoint(self, session, **kwargs):
|
||||
endpoint_data = self.get_endpoint_data(session, **kwargs)
|
||||
if not endpoint_data:
|
||||
return None
|
||||
return endpoint_data.url
|
||||
|
||||
def get_endpoint_data(self, session,
|
||||
endpoint_override=None,
|
||||
discover_versions=True,
|
||||
**kwargs):
|
||||
urlkw = {}
|
||||
for k in ('service_type', 'service_name', 'service_id', 'endpoint_id',
|
||||
'region_name', 'interface'):
|
||||
if k in kwargs:
|
||||
urlkw[k] = kwargs[k]
|
||||
|
||||
endpoint = endpoint_override or self.service_catalog.url_for(**urlkw)
|
||||
return super(_ContextAuthPlugin, self).get_endpoint_data(
|
||||
session, endpoint_override=endpoint,
|
||||
discover_versions=discover_versions, **kwargs)
|
||||
|
||||
|
||||
def get_current():
|
||||
return context.get_current()
|
||||
|
@ -84,6 +84,13 @@ class CommunicationFailure(Base):
|
||||
error_type = 'communication_failure'
|
||||
|
||||
|
||||
class KeystoneCommunicationFailure(CommunicationFailure):
|
||||
"""
|
||||
Raised in case one of the alleged Keystone endpoints fails.
|
||||
"""
|
||||
error_type = 'keystone_communication_failure'
|
||||
|
||||
|
||||
class NeutronCommunicationFailure(CommunicationFailure):
|
||||
"""
|
||||
Raised in case one of the alleged Neutron endpoints fails.
|
||||
@ -142,6 +149,10 @@ class EmptyRequestBody(BadRequest):
|
||||
expected = True
|
||||
|
||||
|
||||
class InvalidProject(BadRequest):
|
||||
error_type = 'invalid_project'
|
||||
|
||||
|
||||
class InvalidUUID(BadRequest):
|
||||
error_type = 'invalid_uuid'
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
from oslo_db import options
|
||||
|
||||
from designate import central
|
||||
from designate.common import keystone
|
||||
import designate
|
||||
import designate.network_api
|
||||
from designate.network_api import neutron
|
||||
@ -57,3 +58,4 @@ def list_opts():
|
||||
yield utils.proxy_group, utils.proxy_opts
|
||||
yield None, service.wsgi_socket_opts
|
||||
yield stt.heartbeat_group, stt.heartbeat_opts
|
||||
yield keystone.keystone_group, keystone.list_opts()
|
||||
|
@ -119,6 +119,7 @@ function configure_designate {
|
||||
if is_service_enabled tls-proxy; then
|
||||
# Set the service port for a proxy to take the original
|
||||
iniset $DESIGNATE_CONF service:api listen ${DESIGNATE_SERVICE_HOST}:${DESIGNATE_SERVICE_PORT_INT}
|
||||
iniset $DESIGNATE_CONF keystone cafile $SSL_BUNDLE_FILE
|
||||
else
|
||||
iniset $DESIGNATE_CONF service:api listen ${DESIGNATE_SERVICE_HOST}:${DESIGNATE_SERVICE_PORT}
|
||||
fi
|
||||
@ -127,6 +128,8 @@ function configure_designate {
|
||||
if is_service_enabled keystone; then
|
||||
iniset $DESIGNATE_CONF service:api auth_strategy keystone
|
||||
configure_auth_token_middleware $DESIGNATE_CONF designate $DESIGNATE_AUTH_CACHE_DIR
|
||||
iniset $DESIGNATE_CONF keystone region_name $REGION_NAME
|
||||
iniset $DESIGNATE_CONF service:api quotas_verify_project_id True
|
||||
fi
|
||||
|
||||
# Logging Configuration
|
||||
@ -161,6 +164,7 @@ function configure_designate_tempest() {
|
||||
iniset $TEMPEST_CONFIG dns_feature_enabled api_admin $DESIGNATE_ENABLE_API_ADMIN
|
||||
iniset $TEMPEST_CONFIG dns_feature_enabled api_v2_root_recordsets True
|
||||
iniset $TEMPEST_CONFIG dns_feature_enabled api_v2_quotas True
|
||||
iniset $TEMPEST_CONFIG dns_feature_enabled api_v2_quotas_verify_project True
|
||||
iniset $TEMPEST_CONFIG dns_feature_enabled bug_1573141_fixed True
|
||||
|
||||
# Tell tempest where are nameservers are.
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Designate can verify validity of the project id when setting quotas for it.
|
||||
This feature is enabled by setting a new configuration option
|
||||
``[service:api]quotas_verify_project_id`` to ``True`` (default is ``False``
|
||||
for backward compatibility).
|
Loading…
x
Reference in New Issue
Block a user