diff options
-rw-r--r-- | demo/wsgi_demo_users.yaml | 15 | ||||
-rwxr-xr-x | src/authn.py | 109 | ||||
-rwxr-xr-x | src/wsgi.py | 63 |
3 files changed, 165 insertions, 22 deletions
diff --git a/demo/wsgi_demo_users.yaml b/demo/wsgi_demo_users.yaml new file mode 100644 index 0000000..49c4795 --- /dev/null +++ b/demo/wsgi_demo_users.yaml @@ -0,0 +1,15 @@ +user1: + pw: pw1 + authz: + sunet.se: r + +user2: + pw: pw2 + authz: + su.se: r + +user3: + pw: pw3 + authz: + sunet.se: rw + su.se: r 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() |