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:
parent
fa9ae37ca7
commit
56651f1fdd
@ -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 = [
|
||||||
|
@ -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:
|
||||||
|
@ -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():
|
||||||
|
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 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()
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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.
|
||||||
|
@ -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…
Reference in New Issue
Block a user