mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 09:02:15 -06:00
add idp-volume persistence (optional);
it keeps track of all seen users/groups by default, but nothing takes effect unless --idp-store=3 or 2
This commit is contained in:
parent
bf11b2a421
commit
d162502c38
|
@ -1093,12 +1093,15 @@ def add_cert(ap, cert_path):
|
|||
|
||||
|
||||
def add_auth(ap):
|
||||
idp_db = os.path.join(E.cfg, "idp.db")
|
||||
ses_db = os.path.join(E.cfg, "sessions.db")
|
||||
ap2 = ap.add_argument_group('IdP / identity provider / user authentication options')
|
||||
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
|
||||
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
|
||||
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
|
||||
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
|
||||
ap2.add_argument("--idp-db", metavar="PATH", type=u, default=idp_db, help="where to store the known IdP users/groups (if you run multiple copyparty instances, make sure they use different DBs)")
|
||||
ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP")
|
||||
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
|
||||
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
|
||||
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
|
||||
|
|
|
@ -21,6 +21,7 @@ from .util import (
|
|||
DEF_MTE,
|
||||
DEF_MTH,
|
||||
EXTS,
|
||||
HAVE_SQLITE3,
|
||||
IMPLICATIONS,
|
||||
MIMES,
|
||||
SQLITE_VER,
|
||||
|
@ -32,6 +33,7 @@ from .util import (
|
|||
afsenc,
|
||||
get_df,
|
||||
humansize,
|
||||
min_ex,
|
||||
odfusion,
|
||||
read_utf8,
|
||||
relchk,
|
||||
|
@ -44,6 +46,9 @@ from .util import (
|
|||
vsplit,
|
||||
)
|
||||
|
||||
if HAVE_SQLITE3:
|
||||
import sqlite3
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
@ -935,6 +940,10 @@ class AuthSrv(object):
|
|||
return False
|
||||
|
||||
self.idp_accs[uname] = gnames
|
||||
try:
|
||||
self._update_idp_db(uname, gname)
|
||||
except:
|
||||
self.log("failed to update the --idp-db:\n%s" % (min_ex(),), 3)
|
||||
|
||||
t = "reinitializing due to new user from IdP: [%r:%r]"
|
||||
self.log(t % (uname, gnames), 3)
|
||||
|
@ -947,6 +956,22 @@ class AuthSrv(object):
|
|||
broker.ask("reload", False, True).get()
|
||||
return True
|
||||
|
||||
def _update_idp_db(self, uname, gname):
|
||||
if not self.args.idp_store:
|
||||
return
|
||||
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
|
||||
db = sqlite3.connect(self.args.idp_db)
|
||||
cur = db.cursor()
|
||||
|
||||
cur.execute("delete from us where un = ?", (uname,))
|
||||
cur.execute("insert into us values (?,?)", (uname, gname))
|
||||
|
||||
db.commit()
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
def _map_volume_idp(
|
||||
self,
|
||||
src: str,
|
||||
|
@ -1095,6 +1120,7 @@ class AuthSrv(object):
|
|||
* any non-zero value from IdP group header
|
||||
* otherwise take --grps / [groups]
|
||||
"""
|
||||
self.load_idp_db(bool(self.idp_accs))
|
||||
ret = {un: gns[:] for un, gns in self.idp_accs.items()}
|
||||
ret.update({zs: [""] for zs in acct if zs not in ret})
|
||||
for gn, uns in grps.items():
|
||||
|
@ -1655,7 +1681,7 @@ class AuthSrv(object):
|
|||
shr = enshare[1:-1]
|
||||
shrs = enshare[1:]
|
||||
if enshare:
|
||||
import sqlite3
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
|
||||
zsd = {"d2d": True, "tcolor": self.args.tcolor}
|
||||
shv = VFS(self.log_func, "", shr, shr, AXS(), zsd)
|
||||
|
@ -2621,6 +2647,40 @@ class AuthSrv(object):
|
|||
zs = str(vol.flags.get("tcolor") or self.args.tcolor)
|
||||
vol.flags["tcolor"] = zs.lstrip("#")
|
||||
|
||||
def load_idp_db(self, quiet=False) -> None:
|
||||
# mutex me
|
||||
level = self.args.idp_store
|
||||
if level < 2 or not self.args.idp_h_usr:
|
||||
return
|
||||
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
|
||||
db = sqlite3.connect(self.args.idp_db)
|
||||
cur = db.cursor()
|
||||
|
||||
gsep = self.args.idp_gsep
|
||||
n = []
|
||||
for uname, gname in cur.execute("select un, gs from us"):
|
||||
if level < 3:
|
||||
if uname in self.idp_accs:
|
||||
continue
|
||||
gname = ""
|
||||
gnames = [x.strip() for x in gsep.split(gname)]
|
||||
gnames.sort()
|
||||
|
||||
# self.idp_usr_gh[uname] = gname
|
||||
self.idp_accs[uname] = gnames
|
||||
n.append(uname)
|
||||
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
if n and not quiet:
|
||||
t = ", ".join(n[:9])
|
||||
if len(n) > 9:
|
||||
t += "..."
|
||||
self.log("found %d IdP users in db (%s)" % (len(n), t))
|
||||
|
||||
def load_sessions(self, quiet=False) -> None:
|
||||
# mutex me
|
||||
if self.args.no_ses:
|
||||
|
@ -2628,7 +2688,7 @@ class AuthSrv(object):
|
|||
self.sesa = {}
|
||||
return
|
||||
|
||||
import sqlite3
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
|
||||
ases = {}
|
||||
blen = (self.args.ses_len // 4) * 4 # 3 bytes in 4 chars
|
||||
|
@ -2675,7 +2735,7 @@ class AuthSrv(object):
|
|||
if self.args.no_ses:
|
||||
return
|
||||
|
||||
import sqlite3
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
|
||||
db = sqlite3.connect(self.args.ses_db)
|
||||
cur = db.cursor()
|
||||
|
|
|
@ -88,6 +88,7 @@ if PY2:
|
|||
range = xrange # type: ignore
|
||||
|
||||
|
||||
VER_IDP_DB = 1
|
||||
VER_SESSION_DB = 1
|
||||
VER_SHARES_DB = 2
|
||||
|
||||
|
@ -258,11 +259,15 @@ class SvcHub(object):
|
|||
self.log("root", "effective %s is %s" % (zs, getattr(args, zs)))
|
||||
|
||||
if args.ah_cli or args.ah_gen:
|
||||
args.idp_store = 0
|
||||
args.no_ses = True
|
||||
args.shr = ""
|
||||
|
||||
if args.idp_store and args.idp_h_usr:
|
||||
self.setup_db("idp")
|
||||
|
||||
if not self.args.no_ses:
|
||||
self.setup_session_db()
|
||||
self.setup_db("ses")
|
||||
|
||||
args.shr1 = ""
|
||||
if args.shr:
|
||||
|
@ -421,26 +426,58 @@ class SvcHub(object):
|
|||
except:
|
||||
pass
|
||||
|
||||
def setup_session_db(self) -> None:
|
||||
if not HAVE_SQLITE3:
|
||||
def _db_onfail_ses(self) -> None:
|
||||
self.args.no_ses = True
|
||||
t = "WARNING: sqlite3 not available; disabling sessions, will use plaintext passwords in cookies"
|
||||
self.log("root", t, 3)
|
||||
|
||||
def _db_onfail_idp(self) -> None:
|
||||
self.args.idp_store = 0
|
||||
|
||||
def setup_db(self, which: str) -> None:
|
||||
"""
|
||||
the "non-mission-critical" databases; if something looks broken then just nuke it
|
||||
"""
|
||||
if which == "ses":
|
||||
native_ver = VER_SESSION_DB
|
||||
db_path = self.args.ses_db
|
||||
desc = "sessions-db"
|
||||
pathopt = "ses-db"
|
||||
sanchk_q = "select count(*) from us"
|
||||
createfun = self._create_session_db
|
||||
failfun = self._db_onfail_ses
|
||||
elif which == "idp":
|
||||
native_ver = VER_IDP_DB
|
||||
db_path = self.args.idp_db
|
||||
desc = "idp-db"
|
||||
pathopt = "idp-db"
|
||||
sanchk_q = "select count(*) from us"
|
||||
createfun = self._create_idp_db
|
||||
failfun = self._db_onfail_idp
|
||||
else:
|
||||
raise Exception("unknown cachetype")
|
||||
|
||||
if not db_path.endswith(".db"):
|
||||
zs = "config option --%s (the %s) was configured to [%s] which is invalid; must be a filepath ending with .db"
|
||||
self.log("root", zs % (pathopt, desc, db_path), 1)
|
||||
raise Exception(BAD_CFG)
|
||||
|
||||
if not HAVE_SQLITE3:
|
||||
failfun()
|
||||
if which == "ses":
|
||||
zs = "disabling sessions, will use plaintext passwords in cookies"
|
||||
elif which == "idp":
|
||||
zs = "disabling idp-db, will be unable to remember IdP-volumes after a restart"
|
||||
self.log("root", "WARNING: sqlite3 not available; %s" % (zs,), 3)
|
||||
return
|
||||
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
|
||||
# policy:
|
||||
# the sessions-db is whatever, if something looks broken then just nuke it
|
||||
|
||||
db_path = self.args.ses_db
|
||||
db_lock = db_path + ".lock"
|
||||
try:
|
||||
create = not os.path.getsize(db_path)
|
||||
except:
|
||||
create = True
|
||||
zs = "creating new" if create else "opening"
|
||||
self.log("root", "%s sessions-db %s" % (zs, db_path))
|
||||
self.log("root", "%s %s %s" % (zs, desc, db_path))
|
||||
|
||||
for tries in range(2):
|
||||
sver = 0
|
||||
|
@ -450,17 +487,19 @@ class SvcHub(object):
|
|||
try:
|
||||
zs = "select v from kv where k='sver'"
|
||||
sver = cur.execute(zs).fetchall()[0][0]
|
||||
if sver > VER_SESSION_DB:
|
||||
zs = "this version of copyparty only understands session-db v%d and older; the db is v%d"
|
||||
raise Exception(zs % (VER_SESSION_DB, sver))
|
||||
if sver > native_ver:
|
||||
zs = "this version of copyparty only understands %s v%d and older; the db is v%d"
|
||||
raise Exception(zs % (desc, native_ver, sver))
|
||||
|
||||
cur.execute("select count(*) from us").fetchone()
|
||||
cur.execute(sanchk_q).fetchone()
|
||||
except:
|
||||
if sver:
|
||||
raise
|
||||
sver = 1
|
||||
self._create_session_db(cur)
|
||||
err = self._verify_session_db(cur, sver, db_path)
|
||||
sver = createfun(cur)
|
||||
|
||||
err = self._verify_db(
|
||||
cur, which, pathopt, db_path, desc, sver, native_ver
|
||||
)
|
||||
if err:
|
||||
tries = 99
|
||||
self.args.no_ses = True
|
||||
|
@ -468,10 +507,10 @@ class SvcHub(object):
|
|||
break
|
||||
|
||||
except Exception as ex:
|
||||
if tries or sver > VER_SESSION_DB:
|
||||
if tries or sver > native_ver:
|
||||
raise
|
||||
t = "sessions-db is unusable; deleting and recreating: %r"
|
||||
self.log("root", t % (ex,), 3)
|
||||
t = "%s is unusable; deleting and recreating: %r"
|
||||
self.log("root", t % (desc, ex), 3)
|
||||
try:
|
||||
cur.close() # type: ignore
|
||||
except:
|
||||
|
@ -486,7 +525,7 @@ class SvcHub(object):
|
|||
pass
|
||||
os.unlink(db_path)
|
||||
|
||||
def _create_session_db(self, cur: "sqlite3.Cursor") -> None:
|
||||
def _create_session_db(self, cur: "sqlite3.Cursor") -> int:
|
||||
sch = [
|
||||
r"create table kv (k text, v int)",
|
||||
r"create table us (un text, si text, t0 int)",
|
||||
|
@ -499,8 +538,31 @@ class SvcHub(object):
|
|||
for cmd in sch:
|
||||
cur.execute(cmd)
|
||||
self.log("root", "created new sessions-db")
|
||||
return 1
|
||||
|
||||
def _verify_session_db(self, cur: "sqlite3.Cursor", sver: int, db_path: str) -> str:
|
||||
def _create_idp_db(self, cur: "sqlite3.Cursor") -> int:
|
||||
sch = [
|
||||
r"create table kv (k text, v int)",
|
||||
r"create table us (un text, gs text)",
|
||||
# username, groups
|
||||
r"create index us_un on us(un)",
|
||||
r"insert into kv values ('sver', 1)",
|
||||
]
|
||||
for cmd in sch:
|
||||
cur.execute(cmd)
|
||||
self.log("root", "created new idp-db")
|
||||
return 1
|
||||
|
||||
def _verify_db(
|
||||
self,
|
||||
cur: "sqlite3.Cursor",
|
||||
which: str,
|
||||
pathopt: str,
|
||||
db_path: str,
|
||||
desc: str,
|
||||
sver: int,
|
||||
native_ver: int,
|
||||
) -> str:
|
||||
# ensure writable (maybe owned by other user)
|
||||
db = cur.connection
|
||||
|
||||
|
@ -512,9 +574,16 @@ class SvcHub(object):
|
|||
except:
|
||||
owner = 0
|
||||
|
||||
if which == "ses":
|
||||
cons = "Will now disable sessions and instead use plaintext passwords in cookies."
|
||||
elif which == "idp":
|
||||
cons = "Each IdP-volume will not become available until its associated user sends their first request."
|
||||
else:
|
||||
raise Exception()
|
||||
|
||||
if not lock_file(db_path + ".lock"):
|
||||
t = "the sessions-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --ses-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now disable sessions and instead use plaintext passwords in cookies."
|
||||
return t % (db_path, owner)
|
||||
t = "the %s [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --%s or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. %s"
|
||||
return t % (desc, db_path, owner, pathopt, cons)
|
||||
|
||||
vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000)))
|
||||
if owner:
|
||||
|
@ -526,9 +595,9 @@ class SvcHub(object):
|
|||
for k, v in vars:
|
||||
cur.execute("insert into kv values(?, ?)", (k, v))
|
||||
|
||||
if sver < VER_SESSION_DB:
|
||||
if sver < native_ver:
|
||||
cur.execute("delete from kv where k='sver'")
|
||||
cur.execute("insert into kv values('sver',?)", (VER_SESSION_DB,))
|
||||
cur.execute("insert into kv values('sver',?)", (native_ver,))
|
||||
|
||||
db.commit()
|
||||
cur.close()
|
||||
|
|
|
@ -158,7 +158,7 @@ class Cfg(Namespace):
|
|||
ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who"
|
||||
ka.update(**{k: 9 for k in ex.split()})
|
||||
|
||||
ex = "db_act forget_ip k304 loris no304 nosubtle re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs"
|
||||
ex = "db_act forget_ip idp_store k304 loris no304 nosubtle re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs"
|
||||
ka.update(**{k: 0 for k in ex.split()})
|
||||
|
||||
ex = "ah_alg bname chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src zipmaxt R RS SR"
|
||||
|
|
Loading…
Reference in a new issue