summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLinus Nordberg <linus@nordberg.se>2021-09-15 15:29:10 +0200
committerLinus Nordberg <linus@nordberg.se>2021-09-15 15:29:10 +0200
commit838d36e7cab2f11322d4c3c211407a73ebc712b9 (patch)
treecfbb667b8232221b1d00cdbb1ab5428b4647ef8f /src
parent6cccfe8cb27fc58f373c069e53f1c8773427d173 (diff)
add simple authentication based on a local yaml file
This allows for mapping username/password pairs to sets of organisations with 'r' or 'rw' permissions. To be replaced with an external service providing a JWT in an HTTP header.
Diffstat (limited to 'src')
-rwxr-xr-xsrc/authn.py109
-rwxr-xr-xsrc/wsgi.py63
2 files changed, 150 insertions, 22 deletions
diff --git a/src/authn.py b/src/authn.py
new file mode 100755
index 0000000..8227e2c
--- /dev/null
+++ b/src/authn.py
@@ -0,0 +1,109 @@
+#! /usr/bin/env python3
+
+import yaml
+
+
+class Authz:
+ def __init__(self, org, perms):
+ self._org = org
+ self._perms = perms
+
+ def dump(self):
+ return "{}: {}".format(self._org, self._perms)
+
+ def read_p(self):
+ return 'r' in self._perms
+
+ def write_p(self):
+ return 'w' in self._perms
+
+class User:
+ def __init__(self, username, pw, authz):
+ self._username = username
+ self._password = pw
+ self._authz = {}
+ for org, perms in authz.items():
+ self._authz[org] = Authz(org, perms)
+
+ def dump(self):
+ return ["{}/{}: {}".format(self._username, self._password, auth.dump())
+ for auth in self._authz.values()]
+
+ def authn_p(self, pw):
+ return pw == self._password
+
+ def orgnames(self):
+ return [x for x in self._authz.keys()]
+
+ def read_perms(self):
+ acc = []
+ for k,v in self._authz.items():
+ if v.read_p():
+ acc.append(k)
+ return acc
+
+ def write_perms(self):
+ acc = []
+ for k,v in self._authz.items():
+ if v.write_p():
+ acc.append(k)
+ return acc
+
+class UserDB:
+ def __init__(self, yamlfile):
+ self._users = {}
+ for u, d in yaml.load(open(yamlfile)).items():
+ self._users[u] = User(u, d['pw'], d['authz'])
+
+ def dump(self):
+ return [u.dump() for u in self._users.values()]
+
+ def user_authn_p(self, username, password):
+ user = self._users.get(username)
+ if not user:
+ return False
+ return user.authn_p(password)
+
+ def orgs_for_user(self, username):
+ return self._users.get(username).orgnames()
+
+ def read_perms(self, username, password):
+ user = self._users.get(username)
+ if not user:
+ return None
+ if not user.authn_p(password):
+ return None
+ return user.read_perms()
+
+ def write_perms(self, username, password):
+ user = self._users.get(username)
+ if not user:
+ return None
+ if not user.authn_p(password):
+ return None
+ return user.write_perms()
+
+
+def self_test():
+ db = UserDB('userdb.yaml')
+ print(db.dump())
+
+ orgs = db.orgs_for_user('user3')
+ assert('sunet.se' in orgs)
+ assert('su.se' in orgs)
+ assert(len(orgs) == 2)
+
+ assert(db.user_authn_p('user3', 'pw3') == True)
+ assert(db.user_authn_p('user3', 'wrongpw') == False)
+
+ rp = db.read_perms('user3', 'pw3')
+ assert(len(rp) == 2)
+ assert('sunet.se' in rp)
+ assert('su.se' in rp)
+
+ wp = db.write_perms('user3', 'pw3')
+ assert(len(wp) == 1)
+ assert('sunet.se' in wp)
+
+if __name__ == '__main__':
+ self_test()
diff --git a/src/wsgi.py b/src/wsgi.py
index aed3513..98efc6f 100755
--- a/src/wsgi.py
+++ b/src/wsgi.py
@@ -8,27 +8,29 @@ from db import DictDB
import time
from base64 import b64decode
+import authn
+
class CollectorResource():
- def __init__(self, db):
+ def __init__(self, db, users):
self._db = db
+ self._users = users
def parse_error(data):
return "I want valid JSON but got this:\n{}\n".format(data)
- def user_authn(self, auth_header, authfun):
+ def user_auth(self, auth_header, authfun):
if not auth_header:
- return None # Fail.
+ return None, None # Fail.
BAlit, b64 = auth_header.split()
if BAlit != "Basic":
- return None # Fail
+ return None, None # Fail
userbytes, pwbytes = b64decode(b64).split(b':')
try:
- user = userbytes.decode('ascii')
+ user = userbytes.decode('utf-8')
+ pw = pwbytes.decode('utf-8')
except:
- return None # Fail
- if authfun(user, pwbytes):
- return user # Success.
- return None # Fail.
+ return None, None # Fail
+ return authfun(user, pw)
class EPGet(CollectorResource):
@@ -37,13 +39,14 @@ class EPGet(CollectorResource):
resp.content_type = falcon.MEDIA_JSON
out = []
- userid = self.user_authn(req.auth, lambda user,_pw: user is not None)
- if not userid:
+ orgs = self.user_auth(req.auth, self._users.read_perms)
+ if not orgs:
resp.status = falcon.HTTP_401
- resp.text = 'Invalid user or password\n'
+ resp.text = 'Invalid username or password\n'
return
- out = [{time.ctime(key): dict} for (key, dict) in self._db.search('domain', dict_val=userid)]
+ for org in orgs:
+ out += [{time.ctime(key): dict} for (key, dict) in self._db.search('domain', dict_val=org)]
resp.text = json.dumps(out) + '\n'
@@ -54,12 +57,15 @@ class EPAdd(CollectorResource):
resp.content_type = falcon.MEDIA_TEXT
self._indata = []
- if self.user_authn(req.auth,
- lambda u,p: u == 'admin' and p == b'admin') is None:
+ orgs = self.user_auth(req.auth, self._users.write_perms)
+ if not orgs:
resp.status = falcon.HTTP_401
resp.text = 'Invalid user or password\n'
return
+ # NOTE: Allowing writing to _any_ org!
+ # TODO: Allow only input where input.domain in orgs == True.
+
# TODO: can we do json.load(req.bounded_stream,
# cls=customDecoder) where our decoder calls JSONDecoder after
# decoding UTF-8?
@@ -100,15 +106,28 @@ def init(url_res_map, addr = '', port = 8000):
def main():
- # Simple demo.
- # Try adding some observations, basic auth admin:admin, and
- # include {"domain": "foo.se"} in some of them.
- # Try retreiving all observations for user 'foo.se' (basic auth
- # foo.se:whatever).
+ # Simple demo. Run it from the demo directory where a sample user
+ # database can be found:
+ #
+ # $ cd demo && ../src/wsgi.py
+ # Serving on port 8000...
+ #
+ # 1. Try adding some observations, basic auth user:pw from
+ # wsgi_demo_users.yaml, including {"domain": "sunet.se"} in at
+ # least one of them:
+ #
+ # $ echo '[{"ip": "192.168.0.1", "port": 80, "domain": "sunet.se"}]' | curl -s -u user3:pw3 --data-binary @- http://localhost:8000/sc/v0/add
+ #
+ # 2. Try retreiving all observations for a user with read access
+ # to 'sunet.se':
+ #
+ # $ curl -s -u user1:pw1 http://localhost:8000/sc/v0/get | json_pp -json_opt utf8,pretty
db = DictDB('wsgi_demo.db')
- httpd = init([('/sc/v0/add', EPAdd(db)),
- ('/sc/v0/get', EPGet(db))])
+ users = authn.UserDB('wsgi_demo_users.yaml')
+
+ httpd = init([('/sc/v0/add', EPAdd(db, users)),
+ ('/sc/v0/get', EPGet(db, users))])
print('Serving on port 8000...')
httpd.serve_forever()