summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimo Sorce <simo@redhat.com>2015-07-09 09:28:27 -0400
committerSimo Sorce <simo@redhat.com>2015-07-09 09:54:06 -0400
commita547415764620791621a08ff8a720a448b8c8848 (patch)
tree0ea077ceb9504de32b9c88233fc8f57878710cb4
parent991e3295432e7e0abb86b6129c91e2d14381e124 (diff)
downloadcustodia-a547415764620791621a08ff8a720a448b8c8848.tar.gz
custodia-a547415764620791621a08ff8a720a448b8c8848.tar.xz
custodia-a547415764620791621a08ff8a720a448b8c8848.zip
Add audit log
The Secrets class now logs any GET/SET/DEL of a key in a audit log file. Signed-off-by: Simo Sorce <simo@redhat.com>
-rw-r--r--custodia/log.py47
-rw-r--r--custodia/root.py2
-rw-r--r--custodia/secrets.py36
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