Add schema for OAuth1 consumer API

Add schema validation on create/update of OAuth1 consumer.

This patch also remove the unnecessary code after schema
validation enforcement.

- Remove the check of `secret` in request body for update
consumer API. It is covered by this schema definition.
 'not': {
     'required': ['secret']
 }

- Remove the check of `description` to consistent with other
entities, such as `user`, `group`. It is covered by the following
schema definition.
 'description': validation.nullable(parameter_types.description)

Partially implements: bp schema-validation-extent

Change-Id: I4d7e6188e8120aa4bcb4a27a22a34d7b395d5f49
This commit is contained in:
Dave Chen 2016-01-13 16:53:10 +08:00
parent 543707bbac
commit 892cbf5025
4 changed files with 94 additions and 8 deletions

View File

@ -102,8 +102,6 @@ class OAuth1(core.Oauth1DriverV8):
def create_consumer(self, consumer): def create_consumer(self, consumer):
consumer['secret'] = uuid.uuid4().hex consumer['secret'] = uuid.uuid4().hex
if not consumer.get('description'):
consumer['description'] = None
session = sql.get_session() session = sql.get_session()
with session.begin(): with session.begin():
consumer_ref = Consumer.from_dict(consumer) consumer_ref = Consumer.from_dict(consumer)

View File

@ -21,11 +21,13 @@ from oslo_utils import timeutils
from keystone.common import controller from keystone.common import controller
from keystone.common import dependency from keystone.common import dependency
from keystone.common import utils from keystone.common import utils
from keystone.common import validation
from keystone.common import wsgi from keystone.common import wsgi
from keystone import exception from keystone import exception
from keystone.i18n import _ from keystone.i18n import _
from keystone import notifications from keystone import notifications
from keystone.oauth1 import core as oauth1 from keystone.oauth1 import core as oauth1
from keystone.oauth1 import schema
from keystone.oauth1 import validator from keystone.oauth1 import validator
@ -56,6 +58,7 @@ class ConsumerCrudV3(controller.V3Controller):
return controller.V3Controller.base_url(context, path=path) return controller.V3Controller.base_url(context, path=path)
@controller.protected() @controller.protected()
@validation.validated(schema.consumer_create, 'consumer')
def create_consumer(self, context, consumer): def create_consumer(self, context, consumer):
ref = self._assign_unique_id(self._normalize_dict(consumer)) ref = self._assign_unique_id(self._normalize_dict(consumer))
initiator = notifications._get_request_audit_info(context) initiator = notifications._get_request_audit_info(context)
@ -63,10 +66,10 @@ class ConsumerCrudV3(controller.V3Controller):
return ConsumerCrudV3.wrap_member(context, consumer_ref) return ConsumerCrudV3.wrap_member(context, consumer_ref)
@controller.protected() @controller.protected()
@validation.validated(schema.consumer_update, 'consumer')
def update_consumer(self, context, consumer_id, consumer): def update_consumer(self, context, consumer_id, consumer):
self._require_matching_id(consumer_id, consumer) self._require_matching_id(consumer_id, consumer)
ref = self._normalize_dict(consumer) ref = self._normalize_dict(consumer)
self._validate_consumer_ref(ref)
initiator = notifications._get_request_audit_info(context) initiator = notifications._get_request_audit_info(context)
ref = self.oauth_api.update_consumer(consumer_id, ref, initiator) ref = self.oauth_api.update_consumer(consumer_id, ref, initiator)
return ConsumerCrudV3.wrap_member(context, ref) return ConsumerCrudV3.wrap_member(context, ref)
@ -90,11 +93,6 @@ class ConsumerCrudV3(controller.V3Controller):
initiator = notifications._get_request_audit_info(context) initiator = notifications._get_request_audit_info(context)
self.oauth_api.delete_consumer(consumer_id, initiator) self.oauth_api.delete_consumer(consumer_id, initiator)
def _validate_consumer_ref(self, consumer):
if 'secret' in consumer:
msg = _('Cannot change consumer secret')
raise exception.ValidationError(message=msg)
@dependency.requires('oauth_api') @dependency.requires('oauth_api')
class AccessTokenCrudV3(controller.V3Controller): class AccessTokenCrudV3(controller.V3Controller):

34
keystone/oauth1/schema.py Normal file
View File

@ -0,0 +1,34 @@
# 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 keystone.common import validation
from keystone.common.validation import parameter_types
_consumer_properties = {
'description': validation.nullable(parameter_types.description)
}
consumer_create = {
'type': 'object',
'properties': _consumer_properties,
'additionalProperties': True
}
consumer_update = {
'type': 'object',
'properties': _consumer_properties,
'not': {
'required': ['secret']
},
'minProperties': 1,
'additionalProperties': True
}

View File

@ -25,6 +25,7 @@ from keystone.credential import schema as credential_schema
from keystone import exception from keystone import exception
from keystone.federation import schema as federation_schema from keystone.federation import schema as federation_schema
from keystone.identity import schema as identity_schema from keystone.identity import schema as identity_schema
from keystone.oauth1 import schema as oauth1_schema
from keystone.policy import schema as policy_schema from keystone.policy import schema as policy_schema
from keystone.resource import schema as resource_schema from keystone.resource import schema as resource_schema
from keystone.tests import unit from keystone.tests import unit
@ -83,6 +84,8 @@ _VALID_ENABLED_FORMATS = [True, False]
_INVALID_ENABLED_FORMATS = ['some string', 1, 0, 'True', 'False'] _INVALID_ENABLED_FORMATS = ['some string', 1, 0, 'True', 'False']
_INVALID_DESC_FORMATS = [False, 1, 2.0]
_VALID_URLS = ['https://example.com', 'http://EXAMPLE.com/v3', _VALID_URLS = ['https://example.com', 'http://EXAMPLE.com/v3',
'http://localhost', 'http://127.0.0.1:5000', 'http://localhost', 'http://127.0.0.1:5000',
'http://1.1.1.1', 'http://255.255.255.255', 'http://1.1.1.1', 'http://255.255.255.255',
@ -2038,3 +2041,56 @@ class FederationProtocolValidationTestCase(unit.BaseTestCase):
self.assertRaises(exception.SchemaValidationError, self.assertRaises(exception.SchemaValidationError,
self.protocol_validator.validate, self.protocol_validator.validate,
request_to_validate) request_to_validate)
class OAuth1ValidationTestCase(unit.BaseTestCase):
"""Test for V3 Identity OAuth1 API validation."""
def setUp(self):
super(OAuth1ValidationTestCase, self).setUp()
create = oauth1_schema.consumer_create
update = oauth1_schema.consumer_update
self.create_consumer_validator = validators.SchemaValidator(create)
self.update_consumer_validator = validators.SchemaValidator(update)
def test_validate_consumer_request_succeeds(self):
"""Test that we validate a consumer request successfully."""
request_to_validate = {'description': uuid.uuid4().hex,
'name': uuid.uuid4().hex}
self.create_consumer_validator.validate(request_to_validate)
self.update_consumer_validator.validate(request_to_validate)
def test_validate_consumer_request_with_no_parameters(self):
"""Test that schema validation with empty request body."""
request_to_validate = {}
self.create_consumer_validator.validate(request_to_validate)
# At least one property should be given.
self.assertRaises(exception.SchemaValidationError,
self.update_consumer_validator.validate,
request_to_validate)
def test_validate_consumer_request_with_invalid_description_fails(self):
"""Exception is raised when `description` as a non-string value."""
for invalid_desc in _INVALID_DESC_FORMATS:
request_to_validate = {'description': invalid_desc}
self.assertRaises(exception.SchemaValidationError,
self.create_consumer_validator.validate,
request_to_validate)
self.assertRaises(exception.SchemaValidationError,
self.update_consumer_validator.validate,
request_to_validate)
def test_validate_update_consumer_request_fails_with_secret(self):
"""Exception raised when secret is given."""
request_to_validate = {'secret': uuid.uuid4().hex}
self.assertRaises(exception.SchemaValidationError,
self.update_consumer_validator.validate,
request_to_validate)
def test_validate_consumer_request_with_none_desc(self):
"""Test that schema validation with None desc."""
request_to_validate = {'description': None}
self.create_consumer_validator.validate(request_to_validate)
self.update_consumer_validator.validate(request_to_validate)