From e6e98bcbd9550ae390b0984fa8e47cc7832bce97 Mon Sep 17 00:00:00 2001 From: Zergling_man Date: Mon, 25 Dec 2023 00:27:23 +1100 Subject: [PATCH] I'm not losing main.py twice -.- --- .gitignore | 1 + backends/README.py | 20 +++++++++++++ backends/file.py | 55 +++++++++++++++++++++++++++++++++++ config.py | 5 ++++ main.py | 4 +++ models.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++ utils.py | 0 7 files changed, 156 insertions(+) create mode 100644 .gitignore create mode 100644 backends/README.py create mode 100644 backends/file.py create mode 100644 config.py create mode 100644 main.py create mode 100644 models.py create mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/backends/README.py b/backends/README.py new file mode 100644 index 0000000..2ec3219 --- /dev/null +++ b/backends/README.py @@ -0,0 +1,20 @@ +""" +Here are the headers that every .py in this folder should implement. +Each file is free to define its own set of config options that will be passed in AFTER the file is imported, under the name "config". It is generally assumed that this will be a dict, but there is no strict requirement, other than "it must be a valid json value". +The init() function will be called with no arguments after config is passed in. +the *token functions are about access tokens, as distinct from invite tokens. +The backend will also have access to utils.py, under the name utils. +""" +def init(): pass + +def load_user(username): pass +def save_user(user): pass +def delete_user(user): pass + +def load_tokens(user): pass +def save_token(token): pass +def delete_token(token): pass + +def load_invites(user): pass +def save_invite(token): pass +def delete_invite(token): pass \ No newline at end of file diff --git a/backends/file.py b/backends/file.py new file mode 100644 index 0000000..1f79dcd --- /dev/null +++ b/backends/file.py @@ -0,0 +1,55 @@ +from os import path +def init(): + if not path.exists(config['path']): raise Exception(f"Backend directory doesn't exist: {config['path']}") + for n in ['users','invite_tokens','access_tokens']: + p=path.join(config['path'],n+'.csv') + globals()[n]=p + if not path.exists(p): open(p,'w').write('\n') + if path.isdir(p): raise Exception(f"Backend file is a directory: {p}") + try: open(p).close() + except PermissionError: raise Exception(f"Backend file can't be read: {p}") + try: open(p,'w').close() + except PermissionError: raise Exception(f"Backend file can't be written: {p}") + +def load_user(username): + for line in linegen(users): + split=line.split(',') + if split[0]==username: return models.User(*split) +def save_user(user): + gen=linegen(users) + for line in gen: + split=line.split(',') + if split[0]==user.username: update(users,{gen.send(True):user.csv}) +def delete_user(user): pass + +def load_tokens(user): pass +def save_token(token): pass +def delete_token(token): pass + +def load_invites(user): pass +def save_invite(token): pass +def delete_invite(token): pass + +def linegen(path): + i=0 + file=open(path) + line=file.readline() + while line: + i+=1 + response=yield line + if response is not None: yield i + line=file.readline() + file.close() + +def update(path,lines): + i=0 + file=open(path) + write=open(f'{path}.tmp','w') + line=file.readline() + while line: + i+=1 + if i in lines: write.write(lines[i]+'\n') + else: write.write(line) + line=file.readline() + file.close() + write.close() \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..896dfbb --- /dev/null +++ b/config.py @@ -0,0 +1,5 @@ +import os +import json +file=os.environ.get('AUTH_CONFIG_FILE') +if not file: raise Exception('AUTH_CONFIG_FILE not set. (It should point to a json file.) Exiting.') +config=json.load(open(file)) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..9b29f30 --- /dev/null +++ b/main.py @@ -0,0 +1,4 @@ +from importlib import import_module +from . import utils, models +from .config import config + diff --git a/models.py b/models.py new file mode 100644 index 0000000..ec52573 --- /dev/null +++ b/models.py @@ -0,0 +1,71 @@ +from dataclasses import dataclass +from datetime import datetime +from hashlib import shake_256 +import uuid + +def phash(pw,salt=None): + if salt is None: salt=uuid.uuid64() + return shake_256(f'{pw}{salt}'.encode('utf-8')).hexdigest(),salt + +class AbstractUser(): + pass # It fixes circular dep on user.invited_by + +@dataclass +class User(AbstractUser): + username: str + password_hash: str + salt: str + invited_by: AbstractUser # Root node will just reference itself + email: str='' + @property + def csv(self): + return ','.join([self.username,self.password_hash,self.salt,self.invited_by.username,self.email]) + def create_inv_token(self,*args,**kwargs): + return InviteToken(*args,**kwargs) + def change_password(self,old_pw:str|None,new_pw:str): pass + def save(self): + backend.save_user(self) + + @classmethod + def login(cls,username:str,password:str): + u=lookup_user(username) + if u is None: raise Exception("User doesn't exist") + if phash(password,u.salt)[0]==u.password_hash: return AccessToken(u) + raise Exception("Incorrect password") + @classmethod + def register(cls,username:str,password:str,email:str|None): + if set([chr(n) for n in range(32)]+[','])&set(username): raise Exception('Invalid username') + u=backend.load_user(username) + if u is not None: raise Exception("User already exists") + u=User(username,*phash(password),email) + u.save() + return u + +@dataclass +class Token(): + value: str + owner: User + def __init__(self,value,owner=None): + if owner is None: + owner=value + self.value=uuid.uuid4() + else: + self.value=uuid.UUID(value) + self.owner=owner + def revoke(self): + backend.delete_token(self) + +@dataclass +class InviteToken(Token): + uses: int + _max_uses: int=-1 + _expires: datetime=None + @property + def max_uses(self): return self._max_uses + @max_uses.setter + def max_uses(self,val): + if val==0: self.revoke() + self._max_uses=val + +@dataclass +class AccessToken(Token): pass \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..e69de29