Skip to content

Commit

Permalink
Merge "Verify project-id when setting quota"
Browse files Browse the repository at this point in the history
  • Loading branch information
Zuul authored and openstack-gerrit committed Aug 31, 2018
2 parents 86a7fa4 + 56651f1 commit d7f1400
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 1 deletion.
3 changes: 3 additions & 0 deletions designate/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
'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 = [
Expand Down
9 changes: 9 additions & 0 deletions designate/api/v2/controllers/quotas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -52,6 +54,13 @@ def patch_one(self, tenant_id):
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:
Expand Down
2 changes: 2 additions & 0 deletions designate/cmd/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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
Expand All @@ -30,6 +31,7 @@
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():
Expand Down
95 changes: 95 additions & 0 deletions designate/common/keystone.py
Original file line number Diff line number Diff line change
@@ -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
48 changes: 47 additions & 1 deletion designate/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -193,6 +196,49 @@ def client_addr(self):
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()
11 changes: 11 additions & 0 deletions designate/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -142,6 +149,10 @@ class EmptyRequestBody(BadRequest):
expected = True


class InvalidProject(BadRequest):
error_type = 'invalid_project'


class InvalidUUID(BadRequest):
error_type = 'invalid_uuid'

Expand Down
2 changes: 2 additions & 0 deletions designate/opts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
4 changes: 4 additions & 0 deletions devstack/plugin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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).

0 comments on commit d7f1400

Please sign in to comment.