diff --git a/examples/config.py b/examples/config.py index f3cc2d0102c07c52075bda428119524811de163e..ae122ffbb16f2d78095fa4085d5732f7fde6c491 100644 --- a/examples/config.py +++ b/examples/config.py @@ -1,3 +1,11 @@ +# Import as needed +from onelogin.saml2.idp_metadata_parser import ( + OneLogin_Saml2_IdPMetadataParser as IdPMetadataParser, +) +from deepmerge import always_merger +from pathlib import Path + + SQLALCHEMY_DATABASE_URI = "postgresql+psycopg:///schilder2000" # To generate a secret key: # % python -c 'import secrets; print(secrets.token_hex())' @@ -11,3 +19,107 @@ PRINTERS = { "Office": "ipps://printserver.example.com:443/printers/Office", "Kitchen": "ipp://kitchenprinter.local:631/ipp/print", } + +REQUIRE_LOGIN = True + + +# See upstream documentation for reference: +# https://flask-multipass.readthedocs.io + +_ldap_config = { + "uri": "ldaps://dc.example.org:636", + "bind_dn": "CN=schilder2000,CN=Service Accounts,CN=Users,DC=example,DC=org", + "bind_password": "hunter2", + "timeout": 30, + "verify_cert": True, + # optional: if not present, uses certifi's CA bundle (if installed) + # "cert_file": "path/to/server/cert", + "starttls": False, + "page_size": 1000, + "uid": "sAMAccountName", + "user_base": "CN=Users,DC=example,DC=org", + "user_filter": "(objectCategory=person)", +} + +_saml_config = always_merger.merge( + IdPMetadataParser.parse_remote( + "https://idp.example.org/realms/owca/protocol/saml/descriptor" + ), + { + "debug": False, + "sp": { + "entityId": "https://schilder2000.example.org/multipass/saml/fsmpi-saml/metadata", + "x509cert": (Path(__file__).parent / "saml-cert.pem").read_text(), + "privateKey": (Path(__file__).parent / "saml-key.pem").read_text(), + # We don’t use the name anyway + "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", + }, + "security": { + # Keycloak wants this, even though it doesn’t say so + "logoutRequestSigned": True, + }, + }, +) + +_oidc_config = { + "client_id": "schilder2000", + "client_secret": "hunter2", + "server_metadata_url": "https://idp.example.org/realms/owca/.well-known/openid-configuration", + "client_kwargs": { + "scope": "openid", + }, +} + +MULTIPASS_AUTH_PROVIDERS = { + "test_auth_provider": { + "type": "static", + "title": "Insecure dummy auth", + "identities": { + "gustav": "hunter2", + }, + }, + "fsmpi-ldap": { + "type": "ldap", + "title": "O.W.C.A. LDAP", + "ldap": _ldap_config, + }, + "fsmpi-saml": { + "type": "saml", + "title": "O.W.C.A. SAML", + "saml_config": _saml_config, + }, + "fsmpi-oidc": { + "type": "authlib", + "title": "O.W.C.A. OIDC", + "authlib_args": _oidc_config, + }, +} + +MULTIPASS_IDENTITY_PROVIDERS = { + "test_identity_provider": { + "type": "static", + "identities": { + "gustav": {}, + }, + }, + "ldap": { + "type": "ldap", + "ldap": _ldap_config, + }, + "saml": { + "type": "saml", + }, + "oidc": { + "type": "authlib", + "title": "OIDC", + }, +} + +MULTIPASS_PROVIDER_MAP = { + "test_auth_provider": "test_identity_provider", + "ldap": "ldap", + "saml": "saml", + "oidc": "oidc", +} + +MULTIPASS_IDENTITY_INFO_KEYS = [] diff --git a/frontend/src/main.less b/frontend/src/main.less index 56468733ef5fce78e6445e901caaf24e5691c9d4..dd1d5f89a94ee66e61bb32aaa4b50424decec4fa 100644 --- a/frontend/src/main.less +++ b/frontend/src/main.less @@ -32,7 +32,7 @@ body { } } -@inputs: ~'input[type = "text"], input[type = "number"], textarea, select'; +@inputs: ~'input[type = "text"], input[type = "number"], input[type = "password"], textarea, select'; @buttons: ~'input[type = "submit"], button, a.button'; body, @{inputs}, @{buttons} { @@ -135,6 +135,13 @@ nav { #nav-links { padding: 0; + flex-grow: 1; + } + + #logout { + @media screen and (min-width: @mobile_threshold) { + margin-left: auto; + } } @media screen and (max-width: @mobile_threshold) { @@ -214,7 +221,7 @@ main { font-size: 1.1rem; } - input[type = "text"], textarea { + input[type = "text"], input[type = "password"], textarea { justify-self: stretch; } diff --git a/pdm.lock b/pdm.lock index db2f26c0657aaa4fce64d9ec68bb2854b96a79c0..412b0e3138b9bceb7103085bd1a7fa6617070f77 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,10 +2,10 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "dev"] +groups = ["default", "auth-ldap", "auth-oauth", "auth-saml", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:63f5da6995402ed21545e227c2419cafb694852ed08e87053164e13708530363" +content_hash = "sha256:a9b84acbfcc9d879802fa8f81de993b8b24c2594642836253ea0b885a33132a4" [[metadata.targets]] requires_python = ">=3.12" @@ -141,6 +141,20 @@ files = [ {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] +[[package]] +name = "authlib" +version = "1.3.2" +requires_python = ">=3.8" +summary = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." +groups = ["auth-oauth"] +dependencies = [ + "cryptography", +] +files = [ + {file = "Authlib-1.3.2-py2.py3-none-any.whl", hash = "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc"}, + {file = "authlib-1.3.2.tar.gz", hash = "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2"}, +] + [[package]] name = "awesomeversion" version = "24.6.0" @@ -261,7 +275,7 @@ name = "cffi" version = "1.17.1" requires_python = ">=3.8" summary = "Foreign Function Interface for Python calling C code." -groups = ["default"] +groups = ["default", "auth-oauth"] dependencies = [ "pycparser", ] @@ -376,6 +390,37 @@ files = [ {file = "cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c"}, ] +[[package]] +name = "cryptography" +version = "43.0.1" +requires_python = ">=3.7" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +groups = ["auth-oauth"] +dependencies = [ + "cffi>=1.12; platform_python_implementation != \"PyPy\"", +] +files = [ + {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, + {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, + {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, + {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, + {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, + {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, + {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, +] + [[package]] name = "cssselect2" version = "0.7.0" @@ -457,6 +502,21 @@ files = [ {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, ] +[[package]] +name = "flask-multipass" +version = "0.5.5" +requires_python = "~=3.8" +summary = "A pluggable solution for multi-backend authentication with Flask" +groups = ["default"] +dependencies = [ + "blinker", + "flask", +] +files = [ + {file = "flask_multipass-0.5.5-py3-none-any.whl", hash = "sha256:ee7cb3d3e2b92ca7864ac824ae6b3943091c58bb01678b55707dde069209f59c"}, + {file = "flask_multipass-0.5.5.tar.gz", hash = "sha256:2ea8a0a8b7171e40a5fce575e99ec796d6af84888970ab44ecb2ab06ef89d86c"}, +] + [[package]] name = "flask-shell-ipython" version = "0.5.3" @@ -676,6 +736,19 @@ files = [ {file = "ipython-8.27.0.tar.gz", hash = "sha256:0b99a2dc9f15fd68692e898e5568725c6d49c527d36a9fb5960ffbdeaa82ff7e"}, ] +[[package]] +name = "isodate" +version = "0.6.1" +summary = "An ISO 8601 date/time/duration parser and formatter" +groups = ["auth-saml"] +dependencies = [ + "six", +] +files = [ + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -730,6 +803,50 @@ files = [ {file = "lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d"}, ] +[[package]] +name = "lxml" +version = "5.3.0" +requires_python = ">=3.6" +summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +groups = ["auth-saml"] +files = [ + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, + {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, + {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, + {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, + {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, + {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -974,12 +1091,37 @@ files = [ {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, ] +[[package]] +name = "pyasn1" +version = "0.6.1" +requires_python = ">=3.8" +summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +groups = ["auth-ldap"] +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +requires_python = ">=3.8" +summary = "A collection of ASN.1-based protocols modules" +groups = ["auth-ldap"] +dependencies = [ + "pyasn1<0.7.0,>=0.4.6", +] +files = [ + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, +] + [[package]] name = "pycparser" version = "2.22" requires_python = ">=3.8" summary = "C parser in Python" -groups = ["default"] +groups = ["default", "auth-oauth"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -1076,6 +1218,20 @@ files = [ {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] +[[package]] +name = "python-ldap" +version = "3.4.4" +requires_python = ">=3.6" +summary = "Python modules for implementing LDAP clients" +groups = ["auth-ldap"] +dependencies = [ + "pyasn1-modules>=0.1.5", + "pyasn1>=0.3.7", +] +files = [ + {file = "python-ldap-3.4.4.tar.gz", hash = "sha256:7edb0accec4e037797705f3a05cbf36a9fde50d08c8f67f2aef99a2628fab828"}, +] + [[package]] name = "python-lsp-jsonrpc" version = "1.1.2" @@ -1155,6 +1311,21 @@ files = [ {file = "python_webpack_boilerplate-1.0.3.tar.gz", hash = "sha256:857580c936b0d465f5df8a420f7c120dbed7d1485c2b2cebd406056e3e029c90"}, ] +[[package]] +name = "python3-saml" +version = "1.16.0" +summary = "Saml Python Toolkit. Add SAML support to your Python software using this library" +groups = ["auth-saml"] +dependencies = [ + "isodate>=0.6.1", + "lxml!=4.7.0,>=4.6.5", + "xmlsec>=1.3.9", +] +files = [ + {file = "python3-saml-1.16.0.tar.gz", hash = "sha256:97c9669aecabc283c6e5fb4eb264f446b6e006f5267d01c9734f9d8bffdac133"}, + {file = "python3_saml-1.16.0-py3-none-any.whl", hash = "sha256:20b97d11b04f01ee22e98f4a38242e2fea2e28fbc7fbc9bdd57cab5ac7fc2d0d"}, +] + [[package]] name = "pytoolconfig" version = "1.3.1" @@ -1293,7 +1464,7 @@ name = "six" version = "1.16.0" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" summary = "Python 2 and 3 compatibility utilities" -groups = ["default", "dev"] +groups = ["default", "auth-saml", "dev"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1531,6 +1702,28 @@ files = [ {file = "wtforms-3.1.2.tar.gz", hash = "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9"}, ] +[[package]] +name = "xmlsec" +version = "1.3.14" +requires_python = ">=3.5" +summary = "Python bindings for the XML Security Library" +groups = ["auth-saml"] +dependencies = [ + "lxml>=3.8", +] +files = [ + {file = "xmlsec-1.3.14-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1eb3dcf244a52f796377112d8f238dbb522eb87facffb498425dc8582a84a6bf"}, + {file = "xmlsec-1.3.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:330147ce59fbe56a9be5b2085d739c55a569f112576b3f1b33681f87416eaf33"}, + {file = "xmlsec-1.3.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed4034939d8566ccdcd3b4e4f23c63fd807fb8763ae5668d59a19e11640a8242"}, + {file = "xmlsec-1.3.14-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a98eadfcb0c3b23ccceb7a2f245811f8d784bd287640dcfe696a26b9db1e2fc0"}, + {file = "xmlsec-1.3.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ff7b2711557c1087b72b0a1a88d82eafbf2a6d38b97309a6f7101d4a7041c3"}, + {file = "xmlsec-1.3.14-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:774d5d1e45f07f953c1cc14fd055c1063f0725f7248b6b0e681f59fd8638934d"}, + {file = "xmlsec-1.3.14-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bd10ca3201f164482775a7ce61bf7ee9aade2e7d032046044dd0f6f52c91d79d"}, + {file = "xmlsec-1.3.14-cp312-cp312-win32.whl", hash = "sha256:19c86bab1498e4c2e56d8e2c878f461ccb6e56b67fd7522b0c8fda46d8910781"}, + {file = "xmlsec-1.3.14-cp312-cp312-win_amd64.whl", hash = "sha256:d0762f4232bce2c7f6c0af329db8b821b4460bbe123a2528fb5677d03db7a4b5"}, + {file = "xmlsec-1.3.14.tar.gz", hash = "sha256:934f804f2f895bcdb86f1eaee236b661013560ee69ec108d29cdd6e5f292a2d9"}, +] + [[package]] name = "yarl" version = "1.11.1" diff --git a/pyproject.toml b/pyproject.toml index f593f05708023c52865baee11bf620e9b8e17d3c..904eae0021085da9c024fb21f00e0c9c78e5e00e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,18 @@ dependencies = [ "flask-weasyprint", "python-webpack-boilerplate", "pyipp @ git+https://github.com/ctalkington/python-ipp", + "Flask-Multipass>=0.5.5", +] + +[project.optional-dependencies] +auth-ldap = [ + "python-ldap~=3.4", +] +auth-saml = [ + "python3-saml~=1.16", +] +auth-oauth = [ + "Authlib~=1.3", ] [tool.pdm.dev-dependencies] diff --git a/schilder2000/__init__.py b/schilder2000/__init__.py index df234a283fdcb3e32a1292b07ad48fca12767d11..a4c9fa3c280fa85c31ba2ee7b03563c4a1572290 100644 --- a/schilder2000/__init__.py +++ b/schilder2000/__init__.py @@ -1,14 +1,16 @@ from pathlib import Path +from flask_multipass import Multipass from flask_sqlalchemy import SQLAlchemy from flask_wtf.csrf import CSRFProtect from webpack_boilerplate.config import setup_jinja2_ext -from .helpers import Flask +from .helpers import Flask, identity_handler, require_login db = SQLAlchemy() csrf = CSRFProtect() +multipass = Multipass() def create_app(): @@ -25,6 +27,17 @@ def create_app(): csrf.init_app(app) + if app.config["REQUIRE_LOGIN"]: + app.config["MULTIPASS_LOGIN_FORM_TEMPLATE"] = "login.html.j2" + app.config["MULTIPASS_LOGIN_SELECTOR_TEMPLATE"] = "login_select.html.j2" + app.config["MULTIPASS_SUCCESS_ENDPOINT"] = "views.index" + multipass.identity_handler(identity_handler) + multipass.init_app(app) + + for k, v in app.view_functions.items(): + if k.startswith("_flaskmultipass_saml_acs_"): + csrf.exempt(v) + app.config.update( { "WEBPACK_LOADER": { @@ -38,12 +51,16 @@ def create_app(): from . import views + if app.config["REQUIRE_LOGIN"]: + views.bp.before_request(require_login) app.register_blueprint(views.bp) from . import instance instance.bp.static_folder = instance_path / "static" instance.bp.template_folder = instance_path / "templates" + if app.config["REQUIRE_LOGIN"]: + instance.bp.before_request(require_login) app.register_blueprint(instance.bp) return app diff --git a/schilder2000/helpers.py b/schilder2000/helpers.py index 1a6a5752bd1ba66d9adb2708a4848347af227971..f4aaf9e6e787614a2e295aac74801332c23f8bb8 100644 --- a/schilder2000/helpers.py +++ b/schilder2000/helpers.py @@ -4,11 +4,16 @@ from flask import ( Flask as _Flask, Blueprint as FlaskBlueprint, current_app, + redirect, render_template, + url_for, + session, ) from jinja2 import BaseLoader, ChoiceLoader, PrefixLoader, Template +from flask_multipass import IdentityInfo + class Blueprint(FlaskBlueprint): def real_template_name( @@ -86,3 +91,17 @@ def get_template_attribute( return getattr(mod, attribute) else: return getattr(mod, attribute, default) + + +def identity_handler(identity_info: IdentityInfo): + session["identity"] = dict( + identifier=identity_info.identifier, + provider=identity_info.provider.name, + secure_login=identity_info.secure_login, + data=identity_info.data, + ) + + +def require_login(): + if "identity" not in session: + return redirect(url_for("login")) diff --git a/schilder2000/templates/_base.html.j2 b/schilder2000/templates/_base.html.j2 index 6e6816345ce79a21870d967f99181e60fceffb1c..49cd22ace8392abe659d7391f7750c3a5d5964f0 100644 --- a/schilder2000/templates/_base.html.j2 +++ b/schilder2000/templates/_base.html.j2 @@ -61,6 +61,9 @@ <div id="nav-links"> <a {{ maybe_active_href("views.index") }}>Startseite</a> <a {{ maybe_active_href("views.create") }}>Neues Schild</a> + {%- if "identity" in session -%} + <a href="{{ url_for('views.logout') }}" id="logout">Abmelden</a> + {%- endif -%} </div> </nav> {%- endblock nav -%} diff --git a/schilder2000/templates/login.html.j2 b/schilder2000/templates/login.html.j2 new file mode 100644 index 0000000000000000000000000000000000000000..452343c3589f52e256470125706d91e5738b9f60 --- /dev/null +++ b/schilder2000/templates/login.html.j2 @@ -0,0 +1,15 @@ +{% extends "_base.html.j2" %} + +{% block title -%} + Anmeldung +{%- endblock title %} + +{% block main -%} + <form method="post"> + {%- for field in form -%} + {{ render_field(field) }} + {%- endfor -%} + + <input type="submit" value="Anmelden" /> + </form> +{%- endblock main %} diff --git a/schilder2000/templates/login_select.html.j2 b/schilder2000/templates/login_select.html.j2 new file mode 100644 index 0000000000000000000000000000000000000000..a3cc5367fcf8b067f1c34844e110d34bc4aa21f4 --- /dev/null +++ b/schilder2000/templates/login_select.html.j2 @@ -0,0 +1,14 @@ +{% extends "_base.html.j2" %} + +{% block title -%} + Anmeldung +{%- endblock title %} + +{% block main -%} + Available login providers: + <ul> + {% for provider in providers|sort(attribute='title') %} + <li><a href="{{ url_for(login_endpoint, provider=provider.name, next=next) }}">{{ provider.title }}</a></li> + {% endfor %} + </ul> +{%- endblock %} diff --git a/schilder2000/views.py b/schilder2000/views.py index fa26e05842519cc40f3df22b13901b14ba46cf7f..dfa02925884af9ecdce957bdb9fc20d255505b93 100644 --- a/schilder2000/views.py +++ b/schilder2000/views.py @@ -1,4 +1,4 @@ -from . import db +from . import db, multipass from .instance import list_templates, list_images from .models import Schild, SchildForm, PrintForm @@ -92,3 +92,7 @@ def create(): "schild.html.j2", form=form, ) + +@bp.route("/logout") +def logout(): + return multipass.logout(url_for(".index"), clear_session=True)