From 64501fd7f13c58ad23232464de6eda8c77e9fc8a Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 8 Dec 2024 17:18:20 +0000 Subject: [PATCH] hybrid IdP (check regular users too); closes #122 previously, when IdP was enabled, the password-based login would be entirely disabled. This was a semi-conscious decision, based on the assumption that you would always want to use IdP after enabling it. it makes more sense to keep password-based login working as usual, conditionally disengaging it for requests which contains a valid IdP username header. This makes it possible to define fallback users, or API-only users, and all similar escape hatches. --- README.md | 4 +++- copyparty/__main__.py | 2 +- copyparty/httpcli.py | 19 ++++++++----------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c47dad89..13f18400 100644 --- a/README.md +++ b/README.md @@ -1488,7 +1488,9 @@ replace copyparty passwords with oauth and such you can disable the built-in password-based login system, and instead replace it with a separate piece of software (an identity provider) which will then handle authenticating / authorizing of users; this makes it possible to login with passkeys / fido2 / webauthn / yubikey / ldap / active directory / oauth / many other single-sign-on contraptions -a popular choice is [Authelia](https://www.authelia.com/) (config-file based), another one is [authentik](https://goauthentik.io/) (GUI-based, more complex) +* the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header + +some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex) there is a [docker-compose example](./docs/examples/docker/idp-authelia-traefik) which is hopefully a good starting point (alternatively see [./docs/idp.md](./docs/idp.md) if you're the DIY type) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index eb97f614..2eb50563 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1083,7 +1083,7 @@ def add_cert(ap, cert_path): def add_auth(ap): 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 and assume the request-header \033[33mHN\033[0m contains the username of the requesting user (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-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") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 48d17676..9ea4c089 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -542,8 +542,14 @@ class HttpCli(object): except: pass + self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw + self.uname = ( + self.asrv.sesa.get(self.pw) + or self.asrv.iacct.get(self.asrv.ah.hash(self.pw)) + or "*" + ) + if self.args.idp_h_usr: - self.pw = "" idp_usr = self.headers.get(self.args.idp_h_usr) or "" if idp_usr: idp_grp = ( @@ -588,20 +594,11 @@ class HttpCli(object): idp_grp = "" if idp_usr in self.asrv.vfs.aread: + self.pw = "" self.uname = idp_usr self.html_head += "\n" else: self.log("unknown username: [%s]" % (idp_usr), 1) - self.uname = "*" - else: - self.uname = "*" - else: - self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw - self.uname = ( - self.asrv.sesa.get(self.pw) - or self.asrv.iacct.get(self.asrv.ah.hash(self.pw)) - or "*" - ) if self.args.ipu and self.uname == "*": self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)]