From 09f22993bef6691dad26d495c0bb91c3db135a5f Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 5 Sep 2025 18:44:30 +0000 Subject: [PATCH] idp login/logout routes (#761) --- README.md | 4 ++++ copyparty/__main__.py | 10 +++++++++- copyparty/authsrv.py | 3 ++- copyparty/httpcli.py | 4 ++-- copyparty/web/browser.js | 26 ++++++++++++++++++++++++-- copyparty/web/splash.css | 1 + copyparty/web/splash.html | 18 +++++++++++++++++- copyparty/web/splash.js | 8 ++++++-- tests/util.py | 4 +++- 9 files changed, 68 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f091d122..e93e4282 100644 --- a/README.md +++ b/README.md @@ -1942,6 +1942,10 @@ you can disable the built-in password-based login system, and instead replace it * the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header + * `--auth-ord` configured auth precedence, for example to allow overriding the IdP with a copyparty password + +* the login/logout links/buttons can be replaced with links to your IdP with `--idp-login` and `--idp-logout` , for example `--idp-login /idp/login/?redir={dst}` will expand `{dst}` to the page the user was on when clicking Login + * if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index fedef803..86a2fdbe 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -897,6 +897,11 @@ def get_sects(): middleware and not by clients! and, as an extra precaution, send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword) and then \033[36m--idp-h-key finalmasterspark\033[0m to require that + + the login/logout links/buttons can be replaced with links + going to your IdP's UI; \033[36m--idp-login /login/?redir={dst}\033[0m + will expand \033[36m{dst}\033[0m to the URL of the current page, so + the IdP can redirect the user back to where they were """ ), ], @@ -1303,6 +1308,9 @@ 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("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page") + ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button") + ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m") 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") @@ -1317,7 +1325,7 @@ def add_auth(ap): 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) + ap2.add_argument("--ao-have-pw", type=u, default="", help=argparse.SUPPRESS) def add_chpw(ap): diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 4adde56e..34a3fe46 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2808,6 +2808,7 @@ class AuthSrv(object): js_htm = { "SPINNER": self.args.spinner, "s_name": self.args.bname, + "idp_login": self.args.idp_login, "have_up2k_idx": "e2d" in vf, "have_acode": not self.args.no_acode, "have_c2flac": self.args.allow_flac, @@ -2879,7 +2880,7 @@ class AuthSrv(object): 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 + self.args.ao_have_pw = pw < 99 or not self.args.have_idp_hdrs def load_idp_db(self, quiet=False) -> None: # mutex me diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 8eaed03d..8e111386 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -690,7 +690,7 @@ class HttpCli(object): if idp_usr in self.asrv.vfs.aread: self.pw = "" self.uname = idp_usr - if self.args.ao_have_pw: + if self.args.ao_have_pw or self.args.idp_logout: self.html_head += "\n" else: self.html_head += "\n" @@ -3051,7 +3051,7 @@ class HttpCli(object): self.asrv.forget_session(self.conn.hsrv.broker, self.uname) self.get_pwd_cookie("x") - dst = self.args.SRS + "?h" + dst = self.args.idp_logout or (self.args.SRS + "?h") h2 = 'continue' html = self.j2s("msg", h1="ok bye", h2=h2, redir=dst) self.reply(html.encode("utf-8")) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 189f0a87..4106f1b6 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -114,6 +114,7 @@ var Ls = { "gou": 'parent folder">up', "gon": 'next folder">next', "logout": "Logout ", + "login": "Login", "access": " access", "ot_close": "close submenu", "ot_search": "search for files by attributes, path / name, music tags, or any combination of those$N$N<code>foo bar</code> = must contain both «foo» and «bar»,$N<code>foo -bar</code> = must contain «foo» but not «bar»,$N<code>^yana .opus$</code> = start with «yana» and be an «opus» file$N<code>"try unite"</code> = contain exactly «try unite»$N$Nthe date format is iso-8601, like$N<code>2009-12-31</code> or <code>2020-09-12 23:30:00</code>", @@ -745,6 +746,7 @@ var Ls = { "gou": 'naviger ett nivå opp">opp', "gon": 'naviger til mappen etter denne">neste', "logout": "Logg ut ", + "login": "Logg inn", "access": " tilgang", "ot_close": "lukk verktøy", "ot_search": "søk etter filer ved å angi filnavn, mappenavn, tid, størrelse, eller metadata som sangtittel / artist / osv.$N$N<code>foo bar</code> = inneholder både «foo» og «bar»,$N<code>foo -bar</code> = inneholder «foo» men ikke «bar»,$N<code>^yana .opus$</code> = starter med «yana», filtype «opus»$N<code>"try unite"</code> = «try unite» eksakt$N$Ndatoformat er iso-8601, så f.eks.$N<code>2009-12-31</code> eller <code>2020-09-12 23:30:00</code>", @@ -1375,6 +1377,7 @@ var Ls = { "gou": '顶部">up', "gon": '下一项">next', "logout": " 登出", + "login": "登录", //m "access": " 访问", "ot_close": "关闭子菜单", "ot_search": "按属性、路径/名称、音乐标签或上述内容的任意组合搜索文件$N$N<code>foo bar</code> = 必须包含 «foo» 和 «bar»,$N<code>foo -bar</code> = 包含 «foo» 而不包含 «bar»,$N<code>^yana .opus$</code> = 以 «yama» 为开头的 «opus» 文件$N<code>"try unite"</code> = 正好包含 «try unite»$N$N时间格式为 iso-8601, 比如:$N<code>2009-12-31</code> or <code>2020-09-12 23:30:00</code>", @@ -2009,6 +2012,7 @@ var Ls = { "gou": 'nadřazená složka">nahoru', "gon": 'následující složka">následující', "logout": "Odhlásit ", + "login": "Přihlásit se", //m "access": " přístup", "ot_close": "zavřít podnabídku", "ot_search": "hledat soubory podle atributů, cesty / názvu, hudebních tagů nebo jejich kombinace$N$N<code>foo bar</code> = musí obsahovat jak «foo» tak «bar»,$N<code>foo -bar</code> = musí obsahovat «foo» ale ne «bar»,$N<code>^yana .opus$</code> = začíná na «yana» a je to «opus» soubor$N<code>"try unite"</code> = obsahuje přesně «try unite»$N$Nformát data je iso-8601, jako$N<code>2009-12-31</code> nebo <code>2020-09-12 23:30:00</code>", @@ -2639,6 +2643,7 @@ var Ls = { "gou": 'zum übergeordneter Ordner springen">hoch', "gon": 'zum nächsten Ordner springen">nächst.', "logout": "Abmelden ", + "login": "Anmelden", //m "access": " Zugriff", "ot_close": "Submenu schliessen", "ot_search": "Dateien nach Attributen, Pfad/Name, Musiktags oder beliebiger Kombination suchen$N$N<code>foo bar</code> = muss «foo» und «bar» enthalten,$N<code>foo -bar</code> = muss «foo» aber nicht «bar» enthalten,$N<code>^yana .opus$</code> = beginnt mit «yana» und ist «opus»-Datei$N<code>"try unite"</code> = genau «try unite» enthalten$N$NDatumsformat ist iso-8601, z.B.$N<code>2009-12-31</code> oder <code>2020-09-12 23:30:00</code>", @@ -3269,6 +3274,7 @@ var Ls = { "gou": 'ylempi hakemisto">ylös', "gon": 'seuraava hakemisto">seur', "logout": "Kirjaudu ulos ", + "login": "Kirjaudu sisään", //m "access": " -oikeudet", "ot_close": "sulje alavalikko", "ot_search": "etsi tiedostoja ominaisuuksien, tiedostopolun tai -nimen, musiikkitägien tai näiden yhdistelmän perusteella$N$N<code>foo bar</code> = täytyy sisältää sekä «foo» että «bar»,$N<code>foo -bar</code> = täytyy sisältää «foo» mutta ei «bar»,$N<code>^yana .opus$</code> = alkaa «yana» ja on «opus»-tiedosto$N<code>"try unite"</code> = sisältää täsmälleen «try unite»$N$Npäivämäärän muoto on iso-8601, kuten$N<code>2009-12-31</code> tai <code>2020-09-12 23:30:00</code>", @@ -3899,6 +3905,7 @@ var Ls = { "gou": 'dossier parent">haut', "gon": 'dossier suivant">suivant', "logout": "Déconnexion ", + "login": "Se connecter", //m "access": " accès", "ot_close": "fermer le sous-menu", "ot_search": "chercher des fichiers par leurs attributs, chemin / nom, tag musicaux, ou nimporte quelle combinaison de ces options$N$N<code>foo bar</code> = doit contenir à la fois «foo» et «bar»,$N<code>foo -bar</code> = doit contenir «foo» mais pas «bar»,$N<code>^yana .opus$</code> = commence par «yana» et est un fichier «opus»$N<code>"try unite"</code> = contient exactement «try unite»$N$Nle format de date est iso-8601, comme$N<code>2009-12-31</code> ou <code>2020-09-12 23:30:00</code>", @@ -4529,6 +4536,7 @@ var Ls = { "gou": 'γονικός φάκελος">πάνω', "gon": 'επόμενος φάκελος">επόμενο', "logout": "Αποσύνδεση ", + "login": "Σύνδεση", //m "access": " πρόσβαση", "ot_close": "κλείσιμο υπομενού", "ot_search": "αναζήτηση αρχείων με βάση χαρακτηριστικά, διαδρομή / όνομα, μουσικά tags ή οποιονδήποτε συνδυασμό$N$N<code>foo bar</code> = πρέπει να περιέχει και τα «foo» και «bar»,$N<code>foo -bar</code> = πρέπει να περιέχει το «foo» αλλά όχι το «bar»,$N<code>^yana .opus$</code> = να ξεκινά με «yana» και να είναι αρχείο «opus»$N<code>"try unite"</code> = να περιέχει ακριβώς «try unite»$N$Nη μορφή ημερομηνίας είναι iso-8601, όπως$N<code>2009-12-31</code> ή <code>2020-09-12 23:30:00</code>", @@ -5159,6 +5167,7 @@ var Ls = { "gou": 'cartella genitore">su', "gon": 'prossima cartella">succ', "logout": "Logout ", + "login": "Accedi", //m "access": " accesso", "ot_close": "chiudi sottomenu", "ot_search": "cerca file per attributi, percorso / nome, tag musicali, o qualsiasi combinazione di questi$N$N<code>foo bar</code> = deve contenere sia «foo» che «bar»,$N<code>foo -bar</code> = deve contenere «foo» ma non «bar»,$N<code>^yana .opus$</code> = inizia con «yana» ed è un file «opus»$N<code>"try unite"</code> = contiene esattamente «try unite»$N$Nil formato data è iso-8601, come$N<code>2009-12-31</code> o <code>2020-09-12 23:30:00</code>", @@ -5789,6 +5798,7 @@ var Ls = { "gou": '상위 폴더">위로', "gon": '다음 폴더">다음', "logout": "로그아웃 ", + "login": "로그인", //m "access": " 액세스", "ot_close": "하위 메뉴 닫기", "ot_search": "속성, 경로/이름, 음악 태그 또는 이들의 조합으로 파일을 검색합니다.$N$N<code>foo bar</code> = «foo»와 «bar»를 모두 포함해야 함,$N<code>foo -bar</code> = «foo»는 포함하지만 «bar»는 포함하지 않아야 함,$N<code>^yana .opus$</code> = «yana»로 시작하고 «opus» 파일이어야 함$N<code>"try unite"</code> = 정확히 «try unite»를 포함해야 함$N$N날짜 형식은 ISO-8601입니다. 예:$N<code>2009-12-31</code> 또는 <code>2020-09-12 23:30:00</code>", @@ -6419,6 +6429,7 @@ var Ls = { "gou": 'Bovenligende map">Omhoog', "gon": 'Volgende map">Volgende', "logout": "Uitloggen ", + "login": "Inloggen", //m "access": " Toegang", "ot_close": "Sluit onder-menu", "ot_search": "Zoek voor bestanden bij attributes, pad / naam, muziek tags, of elk andere combinatie tussen$N$N<code>foo bar</code> = moet beide «foo» en «bar» bevatten,$N<code>foo -bar</code> = moet «foo» bevatten maar geen «bar»,$N<code>^yana .opus$</code> = start met «yana» en moet een «opus» bestand zijn$N<code>"try unite"</code> = moet precies «try unite» bevatten$N$Nde datum formaat is iso-8601, zoals$N<code>2009-12-31</code> of <code>2020-09-12 23:30:00</code>", @@ -7050,6 +7061,7 @@ var Ls = { "gou": 'navigér eitt nivå opp">opp', "gon": 'navigér åt mappa etter den her">neste', "logout": "Logg ut ", + "login": "Logg inn", "access": " åtgang", "ot_close": "lukk reiskap", "ot_search": "søk etter filer ved å angje filnamn, mappenamn, tid, storleik, eller metadata som songtittel / artist / osv.$N$N<code>foo bar</code> = inneheld båe «foo» og «bar»,$N<code>foo -bar</code> = innehold «foo» men ikkje «bar»,$N<code>^yana .opus$</code> = startar med «yana», filtype «opus»$N<code>"try unite"</code> = «try unite» eksakt$N$Ndatoformat er iso-8601, så f.eks.$N<code>2009-12-31</code> eller <code>2020-09-12 23:30:00</code>", @@ -7679,6 +7691,7 @@ var Ls = { "gou": 'nadrzędny folder">w górę', "gon": 'następny folder">następny', "logout": "Wyloguj ", + "login": "Zaloguj się", //m "access": " dostęp", "ot_close": "zamknij pod-menu", "ot_search": "szukaj plików po atrybutach, ścieżce / nazwie, tagach muzyki, bądź dowolnej ich kombinacji$N$N<code>foo bar</code> = musi zawierać «foo» oraz «bar»,$N<code>foo -bar</code> = musi zawierać «foo», lecz nie «bar»,$N<code>^yana .opus$</code> = musi zaczynać się od «yana» i być plikiem «opus»$N<code>"try unite"</code> = zawierać dokładnie «try unite»$N$Nformatem daty jest iso-8601, czyli$N<code>2009-12-31</code> lub <code>2020-09-12 23:30:00</code>", @@ -8307,6 +8320,7 @@ var Ls = { "gou": 'pasta pai">acima', "gon": 'próxima pasta">próximo', "logout": "Sair ", + "login": "Fazer login", "access": " acesso", "ot_close": "fechar submenu", "ot_search": "procurar arquivos por atributos, caminho / nome, tags de música ou qualquer combinação deles$N$N<code>foo bar</code> = deve conter ambos «foo» e «bar»,$N<code>foo -bar</code> = deve conter «foo» mas não «bar»,$N<code>^yana .opus$</code> = começar com «yana» e ser um arquivo «opus»$N<code>"try unite"</code> = conter exatamente «try unite»$N$No formato de data é iso-8601, como$N<code>2009-12-31</code> ou <code>2020-09-12 23:30:00</code>", @@ -8937,6 +8951,7 @@ var Ls = { "gou": 'родительская папка">вверх', "gon": 'следующая папка">след', "logout": "Выйти ", + "login": "Войти", //m "access": " доступ", "ot_close": "закрыть подменю", "ot_search": "искать файлы по атрибутам, пути / имени, музыкальным тегам или любой другой комбинации из следующих конструкций$N$N<code>foo bar</code> = обязано содержать «foo» И «bar»,$N<code>foo -bar</code> = обязано содержать «foo», но не «bar»,$N<code>^yana .opus$</code> = начинается с «yana» и имеет расширение «opus»$N<code>"try unite"</code> = содержит именно «try unite»$N$Nформат времени задаётся по стандарту iso-8601, например$N<code>2009-12-31</code> или <code>2020-09-12 23:30:00</code>", @@ -9567,6 +9582,7 @@ var Ls = { "gou": 'carpeta de nivel superior">subir', "gon": 'siguiente carpeta">siguiente', "logout": "Cerrar sesión ", + "login": "Iniciar sesión", //m "access": " acceso", "ot_close": "cerrar submenú", "ot_search": "buscar archivos por atributos, ruta / nombre, etiquetas de música, o cualquier combinación$N$N<code>foo bar</code> = debe contener «foo» y «bar»,$N<code>foo -bar</code> = debe contener «foo» pero no «bar»,$N<code>^yana .opus$</code> = empieza con «yana» y es un archivo «opus»$N<code>"try unite"</code> = contiene exactamente «try unite»$N$Nel formato de fecha es iso-8601, como$N<code>2009-12-31</code> o <code>2020-09-12 23:30:00</code>", @@ -10196,6 +10212,7 @@ var Ls = { "gou": 'överordnad mapp">upp', "gon": 'nästa mapp">nästa', "logout": "Logga ut ", + "login": "Logga in", //m "access": "-rättighet", "ot_close": "stäng undermeny", "ot_search": "sök efter filer via attribut, sökväg / namn, musiktaggar, eller någon kombination av dessa$N$N<code>foo bar</code> = måste innehålla både «foo» och «bar»,$N<code>foo -bar</code> = måste innehålla «foo» men inte «bar»,$N<code>^yana .opus$</code> = måste börja med «yana» och vara en «opus»-fil$N<code>"try unite"</code> = måste innehålla exakt «try unite»$N$Ndatumformatet är iso-8601, t.ex.$N<code>2009-12-31</code> eller <code>2020-09-12 23:30:00</code>", @@ -10826,6 +10843,7 @@ var Ls = { "gou": 'батьківська папка">вгору', "gon": 'наступна папка">далі', "logout": "Вийти ", + "login": "увійти", //m "access": " доступ", "ot_close": "закрити підменю", "ot_search": "пошук файлів за атрибутами, шляхом / іменем, музичними тегами, або будь-якою комбінацією$N$N<code>foo bar</code> = має містити «foo» і «bar»,$N<code>foo -bar</code> = має містити «foo», але не «bar»,$N<code>^yana .opus$</code> = починатися з «yana» і бути файлом «opus»$N<code>"try unite"</code> = містити точно «try unite»$N$Nформат дати - iso-8601, наприклад$N<code>2009-12-31</code> або <code>2020-09-12 23:30:00</code>", @@ -18289,10 +18307,14 @@ function apply_perms(res) { axs += '-Only'; } + var dst = "?h"; + if (idp_login && acct == "*") + dst = idp_login.replace(/\{dst\}/g, get_evpath()); + ebi('acc_info').innerHTML = '' + srvinf + '' + (acct != '*' ? - '
' : - 'Login'); + '
' : + '' + L.login + ''); var o = QSA('#ops>a[data-perm]'); for (var a = 0; a < o.length; a++) { diff --git a/copyparty/web/splash.css b/copyparty/web/splash.css index 3b4d0f68..fb5d5075 100644 --- a/copyparty/web/splash.css +++ b/copyparty/web/splash.css @@ -38,6 +38,7 @@ a { td a { margin: 0; } +#wb, #w { color: #fff; background: #940; diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html index 61dd973d..1b9a89be 100644 --- a/copyparty/web/splash.html +++ b/copyparty/web/splash.html @@ -21,7 +21,11 @@ {%- if this.uname == '*' %}

howdy stranger   (you're not logged in)

{%- else %} + {%- if this.args.idp_logout %} + logout + {%- else %} logout + {%- endif %}

welcome back, {{ this.uname|e }}

{%- endif %} {%- endif %} @@ -118,6 +122,13 @@ {%- else %}

login for more:

+ {%- if this.args.idp_login %} + + {%- endif %} + {%- if this.args.ao_have_pw %}
{%- if this.args.usernames %} @@ -135,11 +146,16 @@ switch to https {%- endif %}
+ {%- endif %}
{%- endif %}

other stuff: