diff options
author | Simo Sorce <simo@redhat.com> | 2015-04-16 16:44:51 -0400 |
---|---|---|
committer | Simo Sorce <simo@redhat.com> | 2015-04-20 16:07:14 -0400 |
commit | b9e31bf1cc44bdfeaf0454dadb578c4dbb8d588b (patch) | |
tree | 5abfb99f7669f49cdece5c3b15c632130aaf42f4 | |
parent | f77b0158f87a13efc1d315b1bcb58cccf4406e88 (diff) | |
download | custodia-b9e31bf1cc44bdfeaf0454dadb578c4dbb8d588b.tar.gz custodia-b9e31bf1cc44bdfeaf0454dadb578c4dbb8d588b.tar.xz custodia-b9e31bf1cc44bdfeaf0454dadb578c4dbb8d588b.zip |
Move message parsing and validation
Create a message module to deal with message types and validation.
Signed-off-by: Simo Sorce <simo@redhat.com>
-rw-r--r-- | API.md | 3 | ||||
-rw-r--r-- | custodia/message/__init__.py | 0 | ||||
-rw-r--r-- | custodia/message/common.py | 55 | ||||
-rw-r--r-- | custodia/message/formats.py | 49 | ||||
-rw-r--r-- | custodia/message/simple.py | 33 | ||||
-rw-r--r-- | custodia/secrets.py | 54 | ||||
-rw-r--r-- | tests/__init__.py | 0 | ||||
-rw-r--r-- | tests/tests.py (renamed from custodia/tests.py) | 0 |
8 files changed, 168 insertions, 26 deletions
@@ -61,7 +61,7 @@ Returns: - 401 if authentication is necessary - 403 if access to the key is forbidden - 404 if no key was found -- 406 not acceptable, key exists but does not match type requested +- 406 not acceptable, key type unknown/not permitted Storing keys @@ -81,6 +81,7 @@ Returns: - 403 if access to the key is forbidden - 404 one of the elements of the path is not a valid container - 405 if the target is a directory instead of a key (path ends in '/') +- 406 not acceptable, key type unknown/not permitted - 409 if the key already exists diff --git a/custodia/message/__init__.py b/custodia/message/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/custodia/message/__init__.py diff --git a/custodia/message/common.py b/custodia/message/common.py new file mode 100644 index 0000000..89deb59 --- /dev/null +++ b/custodia/message/common.py @@ -0,0 +1,55 @@ +# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file + + +class InvalidMessage(Exception): + """Invalid Message. + + This exception is raised when a message cannot be parsed + or validated. + """ + pass + + +class UnknownMessageType(Exception): + """Unknown Message Type. + + This exception is raised when a message is of an unknown + type. + """ + pass + + +class UnallowedMessage(Exception): + """Unallowed Message. + + This exception is raise when the message type is know but + is not allowed. + """ + pass + + +class MessageHandler(object): + + def __init__(self, request): + self.req = request + self.payload = None + + def parse(self, msg): + """Parses the message. + + :param req: the original request + :param msg: a decoded json string with the incoming message + + :raises InvalidMessage: if the message cannot be parsed or validated + """ + + raise NotImplementedError + + def reply(self, output): + """Generates a reply. + + :param req: the original request + :param output: a json string with the stored output payload + """ + + raise NotImplementedError diff --git a/custodia/message/formats.py b/custodia/message/formats.py new file mode 100644 index 0000000..f8ddc69 --- /dev/null +++ b/custodia/message/formats.py @@ -0,0 +1,49 @@ +# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file + +from custodia.message.common import InvalidMessage +from custodia.message.common import UnknownMessageType +from custodia.message.common import UnallowedMessage +from custodia.message.simple import SimpleKey + + +default_types = ['simple'] + +key_types = {'simple': SimpleKey} + + +class Validator(object): + """Validates incoming messages.""" + + def __init__(self, allowed=None): + """Creates a Validator object. + + :param allowed: list of allowed message types (optional) + """ + self.allowed = allowed or default_types + self.types = dict() + for t in self.allowed: + self.types[t] = key_types[t] + + def add_types(self, types): + self.types.update(types) + + def parse(self, request, msg): + if not isinstance(msg, dict): + raise InvalidMessage('The message must be a dict') + + if 'type' not in msg: + raise InvalidMessage('The type is missing') + + if 'value' not in msg: + raise InvalidMessage('The value is missing') + + if msg['type'] not in list(self.types.keys()): + raise UnknownMessageType("Type '%s' is unknown" % msg['type']) + + if msg['type'] not in self.allowed: + raise UnallowedMessage("Message type '%s' not allowed" % ( + msg['type'],)) + + handler = self.types[msg['type']](request) + handler.parse(msg['value']) + return handler diff --git a/custodia/message/simple.py b/custodia/message/simple.py new file mode 100644 index 0000000..c7f32ee --- /dev/null +++ b/custodia/message/simple.py @@ -0,0 +1,33 @@ +# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file + +from custodia.message.common import InvalidMessage +from custodia.message.common import MessageHandler +from six import string_types +import json + + +class SimpleKey(MessageHandler): + """Handles 'simple' messages""" + + def parse(self, msg): + """Parses a simple message + + :param req: ignored + :param msg: the json-decoded value + + :raises UnknownMessageType: if the type is not 'simple' + :raises InvalidMessage: if the message cannot be parsed or validated + """ + + # On requests we imply 'simple' if there is no input message + if msg is None: + return + + if not isinstance(msg, string_types): + raise InvalidMessage("The 'value' attribute is not a string") + + self.payload = msg + + def reply(self, output): + return json.dumps({'type': 'simple', 'value': output}, + separators=(',', ':')) diff --git a/custodia/secrets.py b/custodia/secrets.py index a6958e3..dd7a76a 100644 --- a/custodia/secrets.py +++ b/custodia/secrets.py @@ -3,6 +3,9 @@ from custodia.httpd.consumer import HTTPConsumer from custodia.httpd.server import HTTPError from custodia.httpd.authorizers import HTTPAuthorizer +from custodia.message.formats import Validator +from custodia.message.common import UnknownMessageType +from custodia.message.common import UnallowedMessage from custodia.store.interface import CSStoreError from custodia.store.interface import CSStoreExists import json @@ -39,6 +42,14 @@ class Namespaces(HTTPAuthorizer): class Secrets(HTTPConsumer): + def __init__(self, *args, **kwargs): + super(Secrets, self).__init__(*args, **kwargs) + self.allowed_keytypes = ['simple'] + if self.config and 'allowed_keytypes' in self.config: + kt = self.config['allowed_keytypes'].split() + self.allowed_keytypes = kt + self._validator = Validator(self.allowed_keytypes) + def _db_key(self, trail): if len(trail) < 2: raise HTTPError(403) @@ -59,19 +70,8 @@ class Secrets(HTTPConsumer): f = self._db_key([default, '']) return f - def _validate(self, value): - try: - msg = json.loads(value) - except Exception: - raise ValueError('Invalid JSON in payload') - if 'type' not in msg: - raise ValueError('Message type missing') - if msg['type'] != 'simple': - raise ValueError('Message type unknown') - if 'value' not in msg: - raise ValueError('Message value missing') - if len(msg.keys()) != 2: - raise ValueError('Unknown attributes in Message') + def _parse(self, request, value): + return self._validator.parse(request, value) def _parent_exists(self, default, trail): # check that the containers exist @@ -182,18 +182,18 @@ class Secrets(HTTPConsumer): response['code'] = 204 def _get_key(self, trail, request, response): - reqtype = request.get('query', dict()).get('type') + # default to simple + query = request.get('query', {'type': 'simple', 'value': ''}) + try: + msg = self._parse(request, query) + except Exception as e: + raise HTTPError(406, str(e)) key = self._db_key(trail) try: output = self.root.store.get(key) if output is None: raise HTTPError(404) - if reqtype is not None: - key = json.loads(output) - keytype = key.get('type') - if keytype != reqtype: - raise HTTPError(406) - response['output'] = output + response['output'] = msg.reply(output) except CSStoreError: raise HTTPError(500) @@ -207,8 +207,12 @@ class Secrets(HTTPConsumer): raise HTTPError(400) value = bytes(body).decode('utf-8') try: - self._validate(value) - except ValueError as e: + msg = self._parse(request, json.loads(value)) + except UnknownMessageType as e: + raise HTTPError(406, str(e)) + except UnallowedMessage as e: + raise HTTPError(406, str(e)) + except Exception as e: raise HTTPError(400, str(e)) # must _db_key first as access control is done here for now @@ -222,7 +226,7 @@ class Secrets(HTTPConsumer): if not ok: raise HTTPError(404) - ok = self.root.store.set(key, value) + ok = self.root.store.set(key, msg.payload) except CSStoreExists: raise HTTPError(409) except CSStoreError: @@ -304,8 +308,8 @@ class SecretsTests(unittest.TestCase): 'trail': ['test', 'key1']} rep = {} self.GET(req, rep) - self.assertEqual(rep['output'], - '{"type":"simple","value":"1234"}') + self.assertEqual(json.loads(rep['output']), + {"type": "simple", "value": "1234"}) def test_3_LISTKeys(self): req = {'remote_user': 'test', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/__init__.py diff --git a/custodia/tests.py b/tests/tests.py index 4eaae13..4eaae13 100644 --- a/custodia/tests.py +++ b/tests/tests.py |