From c6caf96445e26238c08e3739f9d96cb202526d80 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Thu, 19 Oct 2023 04:06:21 +0300 Subject: [PATCH] Added user management to CLI. Mainly for creating admin user or resetting forgotten passwords. --- cista/__main__.py | 55 +++++++++++++++++++++++++++++++++-------- cista/config.py | 29 ++++++++++++++++++++-- cista/pwgen.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 cista/pwgen.py diff --git a/cista/__main__.py b/cista/__main__.py index 2035480..cb2c3ce 100755 --- a/cista/__main__.py +++ b/cista/__main__.py @@ -4,7 +4,7 @@ from pathlib import Path from docopt import docopt -from . import app, config, droppy, serve, httpredir +from . import app, config, droppy, httpredir, pwgen, serve from ._version import version app, httpredir.app # Needed for Sanic multiprocessing @@ -13,6 +13,7 @@ doc = f"""Cista {version} - A file storage for the web. Usage: cista [-c ] [-l ] [--import-droppy] [--dev] [] + cista [-c ] --user [--privileged] [--password] Options: -c CONFDIR Custom config directory @@ -26,6 +27,11 @@ Options: Listen address, path and imported options are preserved in config, and only custom config dir and dev mode need to be specified on subsequent runs. + +User management: + --user NAME Create or modify user + --privileged Give the user full admin rights + --password Reset password """ def main(): @@ -41,6 +47,8 @@ def main(): def _main(): args = docopt(doc) + if args["--user"]: + return _user(args) listen = args["-l"] # Validate arguments first if args[""]: @@ -49,15 +57,7 @@ def _main(): raise ValueError(f"No such directory: {path}") else: path = None - if args["-c"]: - # Custom config directory - confdir = Path(args["-c"]).resolve() - if confdir.exists() and not confdir.is_dir(): - if confdir.name != config.conffile.name: - raise ValueError("Config path is not a directory") - # Accidentally pointed to the cista.toml, use parent - confdir = confdir.parent - config.conffile = config.conffile.with_parent(confdir) + _confdir(args) exists = config.conffile.exists() import_droppy = args["--import-droppy"] necessary_opts = exists or import_droppy or path and listen @@ -88,5 +88,40 @@ def _main(): # Run the server serve.run(dev=dev) +def _confdir(args): + if args["-c"]: + # Custom config directory + confdir = Path(args["-c"]).resolve() + if confdir.exists() and not confdir.is_dir(): + if confdir.name != config.conffile.name: + raise ValueError("Config path is not a directory") + # Accidentally pointed to the cista.toml, use parent + confdir = confdir.parent + config.conffile = config.conffile.with_parent(confdir) + +def _user(args): + _confdir(args) + config.load_config() + name = args["--user"] + if not name or not name.isidentifier(): + raise ValueError("Invalid username") + config.load_config() + u = config.config.users.get(name) + info = f"User {name}" if u else f"New user {name}" + changes = {} + oldadmin = u and u.privileged + if args["--privileged"]: + changes["privileged"] = True + info += " (already admin)" if oldadmin else " (made admin)" + else: + info += " (admin)" if oldadmin else "" + if args["--password"] or not u: + changes["password"] = pw = pwgen.generate() + info += f"\n Password: {pw}" + res = config.update_user(args["--user"], changes) + print(info) + if res == "read": + print(" No changes") + if __name__ == "__main__": sys.exit(main()) diff --git a/cista/config.py b/cista/config.py index 3429c90..1b07a8c 100755 --- a/cista/config.py +++ b/cista/config.py @@ -109,9 +109,34 @@ def load_config(): config = msgspec.toml.decode(conffile.read_bytes(), type=Config, dec_hook=dec_hook) @modifies_config -def update_config(config: Config, changes: dict) -> Config: +def update_config(conf: Config, changes: dict) -> Config: """Create/update the config with new values, respecting changes done by others.""" # Encode into dict, update values with new, convert to Config - settings = {} if config is None else msgspec.to_builtins(config, enc_hook=enc_hook) + settings = {} if conf is None else msgspec.to_builtins(conf, enc_hook=enc_hook) settings.update(changes) return msgspec.convert(settings, Config, dec_hook=dec_hook) + +@modifies_config +def update_user(conf: Config, name: str, changes: dict) -> Config: + """Create/update a user with new values, respecting changes done by others.""" + # Encode into dict, update values with new, convert to Config + try: + u = conf.users[name].__copy__() + except KeyError: + u = User() + if "password" in changes: + from . import auth + auth.set_password(u, changes["password"]) + del changes["password"] + udict = msgspec.to_builtins(u, enc_hook=enc_hook) + udict.update(changes) + settings = msgspec.to_builtins(conf, enc_hook=enc_hook) + settings["users"][name] = msgspec.convert(udict, User, dec_hook=dec_hook) + return msgspec.convert(settings, Config, dec_hook=dec_hook) + +@modifies_config +def del_user(conf: Config, name: str) -> Config: + """Delete named user account.""" + ret = conf.__copy__() + ret.users.pop(name) + return ret diff --git a/cista/pwgen.py b/cista/pwgen.py new file mode 100644 index 0000000..71e344a --- /dev/null +++ b/cista/pwgen.py @@ -0,0 +1,62 @@ +import secrets + + +def generate(n=4): + """Generate a password of random words without repeating any word.""" + wl = list(words) + return ".".join(wl.pop(secrets.randbelow(len(wl))) for i in range(n)) + +# A custom list of 1024 common 3-6 letter words, with unique 3-prefixes and no prefix words, entropy 2.1b/letter 10b/word +words: list = """ +able about absent abuse access acid across act adapt add adjust admit adult advice affair afraid again age agree ahead +aim air aisle alarm album alert alien all almost alone alpha also alter always amazed among amused anchor angle animal +ankle annual answer any apart appear april arch are argue army around array art ascent ash ask aspect assume asthma atom +attack audit august aunt author avoid away awful axis baby back bad bag ball bamboo bank bar base battle beach become +beef before begin behind below bench best better beyond bid bike bind bio birth bitter black bleak blind blood blue +board body boil bomb bone book border boss bottom bounce bowl box boy brain bread bring brown brush bubble buck budget +build bulk bundle burden bus but buyer buzz cable cache cage cake call came can car case catch cause cave celery cement +census cereal change check child choice chunk cigar circle city civil class clean client close club coast code coffee +coil cold come cool copy core cost cotton couch cover coyote craft cream crime cross cruel cry cube cue cult cup curve +custom cute cycle dad damage danger daring dash dawn day deal debate decide deer define degree deity delay demand denial +depth derive design detail device dial dice die differ dim dinner direct dish divert dizzy doctor dog dollar domain +donate door dose double dove draft dream drive drop drum dry duck dumb dune during dust dutch dwarf eager early east +echo eco edge edit effort egg eight either elbow elder elite else embark emerge emily employ enable end enemy engine +enjoy enlist enough enrich ensure entire envy equal era erode error erupt escape essay estate ethics evil evoke exact +excess exist exotic expect extent eye fabric face fade faith fall family fan far father fault feel female fence fetch +fever few fiber field figure file find first fish fit fix flat flesh flight float fluid fly foam focus fog foil follow +food force fossil found fox frame fresh friend frog fruit fuel fun fury future gadget gain galaxy game gap garden gas +gate gauge gaze genius ghost giant gift giggle ginger girl give glass glide globe glue goal god gold good gospel govern +gown grant great grid group grunt guard guess guide gulf gun gym habit hair half hammer hand happy hard hat have hawk +hay hazard head hedge height help hen hero hidden high hill hint hip hire hobby hockey hold home honey hood hope horse +host hotel hour hover how hub huge human hungry hurt hybrid ice icon idea idle ignore ill image immune impact income +index infant inhale inject inmate inner input inside into invest iron island issue italy item ivory jacket jaguar james +jar jazz jeans jelly jewel job joe joke joy judge juice july jump june just kansas kate keep kernel key kick kid kind +kiss kit kiwi knee knife know labor lady lag lake lamp laptop large later laugh lava law layer lazy leader left legal +lemon length lesson letter level liar libya lid life light like limit line lion liquid list little live lizard load +local logic long loop lost loud love low loyal lucky lumber lunch lust luxury lyrics mad magic main major make male +mammal man map market mass matter maze mccoy meadow media meet melt member men mercy mesh method middle milk mimic mind +mirror miss mix mobile model mom monkey moon more mother mouse move much muffin mule must mutual myself myth naive name +napkin narrow nasty nation near neck need nephew nerve nest net never news next nice night noble noise noodle normal +nose note novel now number nurse nut oak obey object oblige obtain occur ocean odor off often oil okay old olive omit +once one onion online open opium oppose option orange orbit order organ orient orphan other outer oval oven own oxygen +oyster ozone pact paddle page pair palace panel paper parade past path pause pave paw pay peace pen people pepper permit +pet philip phone phrase piano pick piece pig pilot pink pipe pistol pitch pizza place please pluck poem point polar pond +pool post pot pound powder praise prefer price profit public pull punch pupil purity push put puzzle qatar quasi queen +quite quoted rabbit race radio rail rally ramp range rapid rare rather raven raw razor real rebel recall red reform +region reject relief remain rent reopen report result return review reward rhythm rib rich ride rifle right ring riot +ripple risk ritual river road robot rocket room rose rotate round row royal rubber rude rug rule run rural sad safe sage +sail salad same santa sauce save say scale scene school scope screen scuba sea second seed self semi sense series settle +seven shadow she ship shock shrimp shy sick side siege sign silver simple since siren sister six size skate sketch ski +skull slab sleep slight slogan slush small smile smooth snake sniff snow soap soccer soda soft solid son soon sort south +space speak sphere spirit split spoil spring spy square state step still story strong stuff style submit such sudden +suffer sugar suit summer sun supply sure swamp sweet switch sword symbol syntax syria system table tackle tag tail talk +tank tape target task tattoo taxi team tell ten term test text that theme this three thumb tibet ticket tide tight tilt +time tiny tip tired tissue title toast today toe toilet token tomato tone tool top torch toss total toward toy trade +tree trial trophy true try tube tumble tunnel turn twenty twice two type ugly unable uncle under unfair unique unlock +until unveil update uphold upon upper upset urban urge usage use usual vacuum vague valid van vapor vast vault vein +velvet vendor very vessel viable video view villa violin virus visit vital vivid vocal voice volume vote voyage wage +wait wall want war wash water wave way wealth web weird were west wet what when whip wide wife will window wire wish +wolf woman wonder wood work wrap wreck write wrong xander xbox xerox xray yang yard year yellow yes yin york you zane +zara zebra zen zero zippo zone zoo zorro zulu +""".split() +assert len(words) == 1024 # Exactly 10 bits of entropy per word