Merge "Verify project-id when setting quota"

This commit is contained in:
Zuul 2018-08-31 12:42:31 +00:00 committed by Gerrit Code Review
commit d7f1400c91
9 changed files with 180 additions and 1 deletions

View File

@ -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 = [

View File

@ -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:

View File

@ -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():

View 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

View File

@ -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()

View File

@ -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'

View File

@ -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()

View File

@ -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.

View File

@ -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).