summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimo Sorce <simo@redhat.com>2015-04-16 16:44:51 -0400
committerSimo Sorce <simo@redhat.com>2015-04-20 16:07:14 -0400
commitb9e31bf1cc44bdfeaf0454dadb578c4dbb8d588b (patch)
tree5abfb99f7669f49cdece5c3b15c632130aaf42f4
parentf77b0158f87a13efc1d315b1bcb58cccf4406e88 (diff)
downloadcustodia-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.md3
-rw-r--r--custodia/message/__init__.py0
-rw-r--r--custodia/message/common.py55
-rw-r--r--custodia/message/formats.py49
-rw-r--r--custodia/message/simple.py33
-rw-r--r--custodia/secrets.py54
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/tests.py (renamed from custodia/tests.py)0
8 files changed, 168 insertions, 26 deletions
diff --git a/API.md b/API.md
index b7395ab..91d9f2b 100644
--- a/API.md
+++ b/API.md
@@ -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