diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 26158879..37427fb1 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -676,6 +676,42 @@ def get_sects(): """ ), ], + [ + "auth-ord", + "authentication precedence", + dedent( + """ + \033[33m--auth-ord\033[0m is a comma-separated list of auth options + (one or more of the [\033[35moptions\033[0m] below); first one wins + + [\033[35mpw\033[0m] is conventional login, for example the "\033[36mPW\033[0m" header, + or the \033[36m?pw=\033[0m[...] URL-suffix, or a valid session cookie + (see \033[33m--help-auth\033[0m) + + [\033[35midp\033[0m] is a username provided in the http-request-header + defined by \033[33m--idp-h-usr\033[0m and/or \033[33m--idp-hm-usr\033[0m, which is + provided by an authentication middleware such as + authentik, authelia, tailscale, ... (see \033[33m--help-idp\033[0m) + + [\033[35midp-h\033[0m] is specifically the \033[33m--idp-h-usr\033[0m header, + [\033[35midp-hm\033[0m] is specifically an \033[33m--idp-hm-usr\033[0m header; + [\033[35midp\033[0m] is the same as [\033[35midp-hm,idp-h\033[0m] + + [\033[35mipu\033[0m] is a mapping from an IP-address to a username, + auto-authing that client-IP to that account + (see the description of \033[36m--ipu\033[0m in \033[33m--help\033[0m) + + NOTE: even if an option (\033[35mpw\033[0m/\033[35mipu\033[0m/...) is not in the list, + it may still be enabled and can still take effect if + none of the other alternatives identify the user + + NOTE: if [\033[35mipu\033[0m] is in the list, it must be FIRST or LAST + + NOTE: if [\033[35mpw\033[0m] is not in the list, the logout-button + will be hidden when any idp feature is enabled + """ + ), + ], [ "flags", "list of volflags", @@ -1254,6 +1290,7 @@ def add_auth(ap): 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("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)") ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)") + ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord") 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)") @@ -1264,6 +1301,10 @@ def add_auth(ap): ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]") ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS) + ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS) + ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS) + ap2.add_argument("--ao-ipu-wins", type=u, default="", help=argparse.SUPPRESS) + ap2.add_argument("--ao-has-pw", type=u, default="", help=argparse.SUPPRESS) def add_chpw(ap): diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 4309ba2c..ff73f19a 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1713,6 +1713,7 @@ class AuthSrv(object): self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr) self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr) + self.setup_auth_ord() self.setup_pwhash(acct) defpw = acct.copy() @@ -2864,6 +2865,18 @@ class AuthSrv(object): zs = str(vol.flags.get("tcolor") or self.args.tcolor) vol.flags["tcolor"] = zs.lstrip("#") + def setup_auth_ord(self) -> None: + ao = [x.strip() for x in self.args.auth_ord.split(",")] + if "idp" in ao: + zi = ao.index("idp") + ao = ao[:zi] + ["idp-hm", "idp-h"] + ao[zi:] + zsl = "pw idp-h idp-hm ipu".split() + pw, h, hm, ipu = [ao.index(x) if x in ao else 99 for x in zsl] + self.args.ao_idp_before_pw = min(h, hm) < pw + self.args.ao_h_before_hm = h < hm + self.args.ao_ipu_wins = ipu == 0 + self.args.ao_have_pw = pw < 99 + def load_idp_db(self, quiet=False) -> None: # mutex me level = self.args.idp_store diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 88a83105..21676f33 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -624,7 +624,9 @@ class HttpCli(object): or "*" ) - if self.args.have_idp_hdrs: + if self.args.have_idp_hdrs and ( + self.uname == "*" or self.args.ao_idp_before_pw + ): idp_usr = "" if self.args.idp_hm_usr: for hn, hmv in self.args.idp_hm_usr_p.items(): @@ -637,9 +639,9 @@ class HttpCli(object): if idp_usr: break for hn in self.args.idp_h_usr: - if idp_usr: + if idp_usr and not self.args.ao_h_before_hm: break - idp_usr = self.headers.get(hn) + idp_usr = self.headers.get(hn) or idp_usr if idp_usr: idp_grp = ( self.headers.get(self.args.idp_h_grp) or "" @@ -688,7 +690,10 @@ class HttpCli(object): if idp_usr in self.asrv.vfs.aread: self.pw = "" self.uname = idp_usr - self.html_head += "\n" + if self.args.ao_have_pw: + self.html_head += "\n" + else: + self.html_head += "\n" zs = self.asrv.ases.get(idp_usr) if zs: self.set_idp_cookie(zs) @@ -696,7 +701,7 @@ class HttpCli(object): self.log("unknown username: %r" % (idp_usr,), 1) if self.args.have_ipu_or_ipr: - if self.args.ipu and self.uname == "*": + if self.args.ipu and (self.uname == "*" or self.args.ao_ipu_wins): self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)] ipr = self.conn.hsrv.ipr if ipr and self.uname in ipr: diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 4ff09fd1..150b7dac 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -834,7 +834,7 @@ if (o1 && o2 && d.lo3) o1.setAttribute("value", d.lo3.format(o2.textContent)); try { - if (is_idp) { + if (is_idp > 1) { var z = ['#l+div', '#l', '#c']; for (var a = 0; a < z.length; a++) QS(z[a]).style.display = 'none'; diff --git a/tests/util.py b/tests/util.py index 9b1a0e27..06f5fd50 100644 --- a/tests/util.py +++ b/tests/util.py @@ -183,6 +183,7 @@ class Cfg(Namespace): v=v or [], c=c, E=E, + auth_ord="idp,ipu", bup_ck="sha512", chmod_d="755", cookie_cmax=8192,