Verify project-id when setting quota

this patch adds several things to attempt (on best effort basis)
to use incoming user token in the quota set request to ask Keystone
if the project id the quota being set on is actualy valid:

- added new [keystone] config section to hold session- and
  adapter-realated options for accessing Keystone enndpoint
- added a token- and service catalog-based user auth plugin to the request
  context
- use the above to construct a keystoneauth adapter for Identity service
  and attempt to GET on projects/{project_id}

  - only if the Keystone v3 catalog endpoint is not found, or
    the request returns 404 NotFound, we raise an error and return it
    as 400 BadRequest to client when attempting to change quotas for
    project.

This behavior is enabled by setting a new
[service:api]quotas-verify-project-id config option to True (default is
False for backward compatibility).

Change-Id: Ib14ee5b5628509b6a93be8b7bd10e734ab19ffee
Depends-On: https://review.openstack.org/580142
Closes-Bug: #1760822
This commit is contained in:
Pavlo Shchelokovskyy 2018-07-03 21:23:19 +00:00
parent fa9ae37ca7
commit 56651f1fdd
9 changed files with 180 additions and 1 deletions

View File

@ -68,6 +68,9 @@ api_v2_opts = [
'means show all results by default'), 'means show all results by default'),
cfg.IntOpt('max-limit-v2', default=1000, cfg.IntOpt('max-limit-v2', default=1000,
help='Max per-page limit for the V2 API'), 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 = [ api_admin_opts = [

View File

@ -14,9 +14,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import pecan import pecan
from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from designate.api.v2.controllers import rest from designate.api.v2.controllers import rest
from designate.common import keystone
from designate.objects.adapters import DesignateAdapter from designate.objects.adapters import DesignateAdapter
from designate.objects import QuotaList from designate.objects import QuotaList
@ -52,6 +54,13 @@ class QuotasController(rest.RestController):
context = request.environ['context'] context = request.environ['context']
body = request.body_dict 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()) quotas = DesignateAdapter.parse('API_v2', body, QuotaList())
for quota in quotas: for quota in quotas:

View File

@ -19,6 +19,7 @@ from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_reports import guru_meditation_report as gmr from oslo_reports import guru_meditation_report as gmr
from designate.common import keystone
from designate import hookpoints from designate import hookpoints
from designate import service from designate import service
from designate import utils from designate import utils
@ -30,6 +31,7 @@ CONF = cfg.CONF
CONF.import_opt('workers', 'designate.api', group='service:api') CONF.import_opt('workers', 'designate.api', group='service:api')
CONF.import_opt('threads', 'designate.api', group='service:api') CONF.import_opt('threads', 'designate.api', group='service:api')
cfg.CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token') cfg.CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token')
keystone.register_keystone_opts(CONF)
def main(): 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 itertools
import copy import copy
from keystoneauth1.access import service_catalog as ksa_service_catalog
from keystoneauth1 import plugin
from oslo_context import context from oslo_context import context
from oslo_log import log as logging 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, def __init__(self, service_catalog=None, all_tenants=False, abandon=None,
tsigkey_id=None, original_tenant=None, tsigkey_id=None, original_tenant=None,
edit_managed_records=False, hide_counts=False, edit_managed_records=False, hide_counts=False,
client_addr=None, **kwargs): client_addr=None, user_auth_plugin=None, **kwargs):
super(DesignateContext, self).__init__(**kwargs) super(DesignateContext, self).__init__(**kwargs)
self.user_auth_plugin = user_auth_plugin
self.service_catalog = service_catalog self.service_catalog = service_catalog
self.tsigkey_id = tsigkey_id self.tsigkey_id = tsigkey_id
@ -193,6 +196,49 @@ class DesignateContext(context.RequestContext):
def client_addr(self, value): def client_addr(self, value):
self._client_addr = 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(): def get_current():
return context.get_current() return context.get_current()

View File

@ -84,6 +84,13 @@ class CommunicationFailure(Base):
error_type = 'communication_failure' 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): class NeutronCommunicationFailure(CommunicationFailure):
""" """
Raised in case one of the alleged Neutron endpoints fails. Raised in case one of the alleged Neutron endpoints fails.
@ -138,6 +145,10 @@ class EmptyRequestBody(BadRequest):
expected = True expected = True
class InvalidProject(BadRequest):
error_type = 'invalid_project'
class InvalidUUID(BadRequest): class InvalidUUID(BadRequest):
error_type = 'invalid_uuid' error_type = 'invalid_uuid'

View File

@ -17,6 +17,7 @@
from oslo_db import options from oslo_db import options
from designate import central from designate import central
from designate.common import keystone
import designate import designate
import designate.network_api import designate.network_api
from designate.network_api import neutron from designate.network_api import neutron
@ -57,3 +58,4 @@ def list_opts():
yield utils.proxy_group, utils.proxy_opts yield utils.proxy_group, utils.proxy_opts
yield None, service.wsgi_socket_opts yield None, service.wsgi_socket_opts
yield stt.heartbeat_group, stt.heartbeat_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 if is_service_enabled tls-proxy; then
# Set the service port for a proxy to take the original # 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 service:api listen ${DESIGNATE_SERVICE_HOST}:${DESIGNATE_SERVICE_PORT_INT}
iniset $DESIGNATE_CONF keystone cafile $SSL_BUNDLE_FILE
else else
iniset $DESIGNATE_CONF service:api listen ${DESIGNATE_SERVICE_HOST}:${DESIGNATE_SERVICE_PORT} iniset $DESIGNATE_CONF service:api listen ${DESIGNATE_SERVICE_HOST}:${DESIGNATE_SERVICE_PORT}
fi fi
@ -127,6 +128,8 @@ function configure_designate {
if is_service_enabled keystone; then if is_service_enabled keystone; then
iniset $DESIGNATE_CONF service:api auth_strategy keystone iniset $DESIGNATE_CONF service:api auth_strategy keystone
configure_auth_token_middleware $DESIGNATE_CONF designate $DESIGNATE_AUTH_CACHE_DIR 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 fi
# Logging Configuration # 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_admin $DESIGNATE_ENABLE_API_ADMIN
iniset $TEMPEST_CONFIG dns_feature_enabled api_v2_root_recordsets True 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 True
iniset $TEMPEST_CONFIG dns_feature_enabled api_v2_quotas_verify_project True
iniset $TEMPEST_CONFIG dns_feature_enabled bug_1573141_fixed True iniset $TEMPEST_CONFIG dns_feature_enabled bug_1573141_fixed True
# Tell tempest where are nameservers are. # 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).