summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimo Sorce <simo@redhat.com>2015-04-07 22:44:54 -0400
committerSimo Sorce <simo@redhat.com>2015-04-08 00:19:20 -0400
commitf5e002a3d066ed29e5cf4154b6dfa6fd1732785b (patch)
tree153344078cecb2fd82e6d089a8e8360b28d43d4c
parent0c8c416289514889ec095c203880a8ce1e4c23d4 (diff)
downloadcustodia-f5e002a3d066ed29e5cf4154b6dfa6fd1732785b.tar.gz
custodia-f5e002a3d066ed29e5cf4154b6dfa6fd1732785b.tar.xz
custodia-f5e002a3d066ed29e5cf4154b6dfa6fd1732785b.zip
Add basic framework for authorization plugins
-rw-r--r--custodia.conf4
-rwxr-xr-xcustodia/custodia29
-rw-r--r--custodia/httpd/authenticators.py21
-rw-r--r--custodia/httpd/authorizers.py48
-rw-r--r--custodia/httpd/server.py23
5 files changed, 91 insertions, 34 deletions
diff --git a/custodia.conf b/custodia.conf
index 15c86c4..a9009f7 100644
--- a/custodia.conf
+++ b/custodia.conf
@@ -10,6 +10,10 @@ server_version = "Secret/0.0.7"
handler = custodia.httpd.authenticators.SimpleHeaderAuth
name = REMOTE_USER
+[authz:paths]
+handler = custodia.httpd.authorizers.SimplePathAuthz
+paths = /
+
[store:simple]
handler = custodia.store.sqlite.SqliteStore
dburi = secrets.db
diff --git a/custodia/custodia b/custodia/custodia
index d062790..4114a4f 100755
--- a/custodia/custodia
+++ b/custodia/custodia
@@ -25,6 +25,16 @@ def source_config():
raise IOError("Configuration file not found")
return cfgfile
+def attach_store(typename, plugins, stores):
+ for name, c in six.iteritems(plugins):
+ if getattr(c, 'store_name', None) is None:
+ continue
+ try:
+ c.store = stores[c.store_name]
+ except KeyError:
+ raise ValueError('[%s%s] references unexisting store '
+ '"%s"' % (typename, name, c.store_name))
+
def parse_config(cfgfile):
parser = RawConfigParser()
parser.optionxform = str
@@ -32,7 +42,8 @@ def parse_config(cfgfile):
if len(files) == 0:
raise IOError("Failed to read config file")
- config = {'authenticators': dict(), 'consumers': dict(), 'stores': dict()}
+ config = {'authenticators': dict(), 'authorizers': dict(),
+ 'consumers': dict(), 'stores': dict()}
for s in parser.sections():
if s == 'global':
for opt, val in parser.items(s):
@@ -49,6 +60,9 @@ def parse_config(cfgfile):
if s.startswith('auth:'):
menu = 'authenticators'
name = s[5:]
+ elif s.startswith('authz:'):
+ menu = 'authorizers'
+ name = s[6:]
elif s.startswith('store:'):
menu = 'stores'
name = s[6:]
@@ -76,18 +90,13 @@ def parse_config(cfgfile):
hconf[opt] = val
config[menu][name] = handler(hconf)
- # Attach stores to consumers
- for name, c in six.iteritems(config['consumers']):
- if c.store_name is not None:
- try:
- c.store = config['stores'][c.store_name]
- except KeyError:
- raise ValueError('Consumer "%s" references unexisting '
- 'store "%s"' % (name, c.store_name))
+ # Attach stores to other plugins
+ attach_store('auth:', config['authenticators'], config['stores'])
+ attach_store('authz:', config['authorizers'], config['stores'])
+ attach_store('', config['consumers'], config['stores'])
return config
-
if __name__ == '__main__':
cfgfile = source_config()
config = parse_config(cfgfile)
diff --git a/custodia/httpd/authenticators.py b/custodia/httpd/authenticators.py
index cf8402f..de722e0 100644
--- a/custodia/httpd/authenticators.py
+++ b/custodia/httpd/authenticators.py
@@ -1,7 +1,6 @@
# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file
from custodia.httpd.server import HTTPError
-import os
class HTTPAuthenticator(object):
@@ -62,23 +61,3 @@ class SimpleHeaderAuth(HTTPAuthenticator):
request['remote_user'] = value
return True
-
-
-class SimpleNULLAuth(HTTPAuthenticator):
-
- def __init__(self, config=None):
- super(SimpleNULLAuth, self).__init__(config)
- self.paths = []
- if 'paths' in self.config:
- self.paths = self.config['paths'].split()
-
- def handle(self, request):
- path = request.get('path', '')
- while path != '':
- if path in self.paths:
- return True
- if path == '/':
- path = ''
- else:
- path, _ = os.path.split(path)
- return None
diff --git a/custodia/httpd/authorizers.py b/custodia/httpd/authorizers.py
new file mode 100644
index 0000000..bc4f009
--- /dev/null
+++ b/custodia/httpd/authorizers.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file
+
+import os
+
+
+class HTTPAuthorizer(object):
+
+ def __init__(self, config=None):
+ self.config = config
+ self.store_name = None
+ if self.config and 'store' in self.config:
+ self.store_name = self.config['store']
+ self.store = None
+
+ def handle(self, request):
+ raise NotImplementedError
+
+
+class SimplePathAuthz(HTTPAuthorizer):
+
+ def __init__(self, config=None):
+ super(SimplePathAuthz, self).__init__(config)
+ self.paths = []
+ if 'paths' in self.config:
+ self.paths = self.config['paths'].split()
+
+ def handle(self, request):
+ path = request.get('path', '')
+
+ # if an authorized path does not end in /
+ # check if it matches fullpath for strict match
+ for authz in self.paths:
+ if authz.endswith('/'):
+ continue
+ if authz.endswith('.'):
+ # special case to match a path ending in /
+ authz = authz[:-1]
+ if authz == path:
+ return True
+
+ while path != '':
+ if path in self.paths:
+ return True
+ if path == '/':
+ path = ''
+ else:
+ path, _ = os.path.split(path)
+ return None
diff --git a/custodia/httpd/server.py b/custodia/httpd/server.py
index a5e59a9..9ae3f68 100644
--- a/custodia/httpd/server.py
+++ b/custodia/httpd/server.py
@@ -62,9 +62,15 @@ class ForkingLocalHTTPServer(ForkingMixIn, UnixStreamServer):
can add attributes to the request object for use of authorization or
other plugins.
- Once authentication is successful the pipeline will parse the path
- component and find the consumer plugin that handles the provided path
- walking up the path component by component until a consumer is found.
+ When authorization is performed and positive result will cause the
+ operation to be accepted and any negative result will cause it to fail.
+ If no authorization plugin returns a positive result a 403 error is
+ returned.
+
+ Once authentication and authorization are successful the pipeline will
+ parse the path component and find the consumer plugin that handles the
+ provided path walking up the path component by component until a
+ consumer is found.
Paths are walked up from the leaf to the root, so if two consumers hang
on the same tree, the one closer to the leaf will be used. If there is
@@ -105,6 +111,17 @@ class ForkingLocalHTTPServer(ForkingMixIn, UnixStreamServer):
if valid_once is not True:
raise HTTPError(403)
+ # auhz framework here
+ authzers = self.config.get('authorizers')
+ if authzers is None:
+ raise HTTPError(403)
+ for authz in authzers:
+ valid = authzers[authz].handle(request)
+ if valid is not None:
+ break
+ if valid is not True:
+ raise HTTPError(403)
+
# Select consumer
path = request.get('path', '')
if not os.path.isabs(path):