diff options
-rw-r--r-- | custodia/log.py | 47 | ||||
-rw-r--r-- | custodia/root.py | 2 | ||||
-rw-r--r-- | custodia/secrets.py | 36 |
3 files changed, 80 insertions, 5 deletions
diff --git a/custodia/log.py b/custodia/log.py index 12a6ba7..3f258e5 100644 --- a/custodia/log.py +++ b/custodia/log.py @@ -17,11 +17,15 @@ def stacktrace(): return f.getvalue() +def get_time(): + t = time.gmtime(time.time()) + return '%04d/%02d/%02d %02d:%02d:%02d' % ( + t[0], t[1], t[2], t[3], t[4], t[5]) + + def error(msg, head=None): if head is not None: - t = time.gmtime(time.time()) - head = '%04d/%02d/%02d %02d:%02d:%02d' % ( - t[0], t[1], t[2], t[3], t[4], t[5]) + head = get_time() sys.stderr.write('[%s] %s\n' % (head, msg)) @@ -29,3 +33,40 @@ def debug(msg): if DEBUG: error(msg, 'DEBUG') sys.stderr.write(stacktrace()) + + +AUDIT_NONE = 0 +AUDIT_GET_ALLOWED = 1 +AUDIT_GET_DENIED = 2 +AUDIT_SET_ALLOWED = 3 +AUDIT_SET_DENIED = 4 +AUDIT_DEL_ALLOWED = 5 +AUDIT_DEL_DENIED = 6 +AUDIT_LAST = 7 +AUDIT_MESSAGES = [ + "AUDIT FAILURE", + "ALLOWED: '{client:s}' requested key '{key:s}'", # AUDIT_GET_ALLOWED + "DENIED: '{client:s}' requested key '{key:s}'", # AUDIT_GET_DENIED + "ALLOWED: '{client:s}' stored key '{key:s}'", # AUDIT_SET_ALLOWED + "DENIED: '{client:s}' stored key '{key:s}'", # AUDIT_SET_DENIED + "ALLOWED: '{client:s}' deleted key '{key:s}'", # AUDIT_DEL_ALLOWED + "DENIED: '{client:s}' deleted key '{key:s}'", # AUDIT_DEL_DENIED +] + + +class audit_log(object): + + def __init__(self, config): + if config is None: + config = {} + self.logfile = config.get('auditlog', 'custodia.audit.log') + + def _log(self, message): + with open(self.logfile, 'a+') as f: + f.write('%s: %s\n' % (get_time(), message)) + f.flush() + + def key_access(self, action, client, keyname): + if action <= AUDIT_NONE or action >= AUDIT_LAST: + action = AUDIT_NONE + self._log(AUDIT_MESSAGES[action].format(client=client, key=keyname)) diff --git a/custodia/root.py b/custodia/root.py index 93b2e8b..4cde152 100644 --- a/custodia/root.py +++ b/custodia/root.py @@ -10,7 +10,7 @@ class Root(HTTPConsumer): def __init__(self, *args, **kwargs): super(Root, self).__init__(*args, **kwargs) if self.store_name is not None: - self.add_sub('secrets', Secrets()) + self.add_sub('secrets', Secrets(self.config)) def GET(self, request, response): return json.dumps({'message': "Quis custodiet ipsos custodes?"}) diff --git a/custodia/secrets.py b/custodia/secrets.py index 2497a90..9f894a8 100644 --- a/custodia/secrets.py +++ b/custodia/secrets.py @@ -8,6 +8,7 @@ from custodia.message.common import UnknownMessageType from custodia.message.common import UnallowedMessage from custodia.store.interface import CSStoreError from custodia.store.interface import CSStoreExists +from custodia import log import json import os @@ -49,6 +50,7 @@ class Secrets(HTTPConsumer): kt = self.config['allowed_keytypes'].split() self.allowed_keytypes = kt self._validator = Validator(self.allowed_keytypes) + self._auditlog = log.audit_log(self.config) def _db_key(self, trail): if len(trail) < 2: @@ -180,7 +182,30 @@ class Secrets(HTTPConsumer): response['code'] = 204 + def _client_name(self, request): + if 'remote_user' in request: + return request['remote_user'] + elif 'creds' in request: + creds = request['creds'] + return '<pid={pid:d} uid={uid:d} gid={gid:d}>'.format(**creds) + else: + return 'Unknown' + + def _audit(self, ok, fail, fn, trail, request, response): + action = fail + client = self._client_name(request) + key = '/'.join(trail) + try: + fn(trail, request, response) + action = ok + finally: + self._auditlog.key_access(action, client, key) + def _get_key(self, trail, request, response): + self._audit(log.AUDIT_GET_ALLOWED, log.AUDIT_GET_DENIED, + self._int_get_key, trail, request, response) + + def _int_get_key(self, trail, request, response): # default to simple query = request.get('query', '') if len(query) == 0: @@ -200,6 +225,10 @@ class Secrets(HTTPConsumer): raise HTTPError(500) def _set_key(self, trail, request, response): + self._audit(log.AUDIT_SET_ALLOWED, log.AUDIT_SET_DENIED, + self._int_set_key, trail, request, response) + + def _int_set_key(self, trail, request, response): content_type = request.get('headers', dict()).get('Content-Type', '') if content_type.split(';')[0].strip() != 'application/json': @@ -238,6 +267,10 @@ class Secrets(HTTPConsumer): response['code'] = 201 def _del_key(self, trail, request, response): + self._audit(log.AUDIT_DEL_ALLOWED, log.AUDIT_DEL_DENIED, + self._int_del_key, trail, request, response) + + def _int_del_key(self, trail, request, response): key = self._db_key(trail) try: ret = self.root.store.cut(key) @@ -258,13 +291,14 @@ class SecretsTests(unittest.TestCase): @classmethod def setUpClass(cls): - cls.secrets = Secrets() + cls.secrets = Secrets({'auditlog': 'test.audit.log'}) cls.secrets.root.store = SqliteStore({'dburi': 'testdb.sqlite'}) cls.authz = Namespaces({}) @classmethod def tearDownClass(self): try: + os.unlink('test.audit.log') os.unlink('testdb.sqlite') except OSError: pass |