From 4a94f515b38b564101620e5458019cf7355f7a50 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 18 Feb 2021 21:24:34 +0100 Subject: [PATCH 01/34] root: add next branch --- azure-pipelines.yml | 1 + outpost/azure-pipelines.yml | 1 + web/azure-pipelines.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3adc8b401..f087d013a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,5 +1,6 @@ trigger: - master + - next resources: - repo: self diff --git a/outpost/azure-pipelines.yml b/outpost/azure-pipelines.yml index 410df0d9a..012784ce3 100644 --- a/outpost/azure-pipelines.yml +++ b/outpost/azure-pipelines.yml @@ -1,5 +1,6 @@ trigger: - master + - next variables: ${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}: diff --git a/web/azure-pipelines.yml b/web/azure-pipelines.yml index 32aaec773..45ba50d2f 100644 --- a/web/azure-pipelines.yml +++ b/web/azure-pipelines.yml @@ -1,5 +1,6 @@ trigger: - master + - next variables: ${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}: From 7ba44b15a71beb35560be1eb18f25a03fe1b24bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Feb 2021 09:21:26 +0100 Subject: [PATCH 02/34] build(deps): bump @sentry/tracing from 6.1.0 to 6.2.0 in /web (#573) Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 6.1.0 to 6.2.0. - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/6.1.0...6.2.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package-lock.json | 48 +++++++++++++++++++++---------------------- web/package.json | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 40ecca3c6..ff7bc9fae 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -229,12 +229,12 @@ } }, "@sentry/hub": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.1.0.tgz", - "integrity": "sha512-JnBSCgNg3VHiMojUl5tCHU8iWPVuE+qqENIzG9A722oJms1kKWBvWl+yQzhWBNdgk5qeAY3F5UzKWJZkbJ6xow==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.2.0.tgz", + "integrity": "sha512-BDTEFK8vlJydWXp/KMX0stvv73V7od224iLi+w3k7BcPwMKXBuURBXPU8d5XIC4G8nwg8X6cnDvwL+zBBlBbkg==", "requires": { - "@sentry/types": "6.1.0", - "@sentry/utils": "6.1.0", + "@sentry/types": "6.2.0", + "@sentry/utils": "6.2.0", "tslib": "^1.9.3" }, "dependencies": { @@ -246,12 +246,12 @@ } }, "@sentry/minimal": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.1.0.tgz", - "integrity": "sha512-g6sfNKenL7wnsr/tibp8nFiMv/XRH0s0Pt4p151npmNI+SmjuUz3GGYEXk8ChCyaKldYKilkNOFdVXJxUf5gZw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.2.0.tgz", + "integrity": "sha512-haxsx8/ZafhZUaGeeMtY7bJt9HbDlqeiaXrRMp1CxGtd0ZRQwHt60imEjl6IH1I73SEWxNfqScGsX2s3HzztMg==", "requires": { - "@sentry/hub": "6.1.0", - "@sentry/types": "6.1.0", + "@sentry/hub": "6.2.0", + "@sentry/types": "6.2.0", "tslib": "^1.9.3" }, "dependencies": { @@ -263,14 +263,14 @@ } }, "@sentry/tracing": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.1.0.tgz", - "integrity": "sha512-s6a4Ra3hHn4awiNz4fOEK6TCV2w2iLcxdppijcYEB7S/1rJpmqZgHWDicqufbOmVMOLmyKLEQ7w+pZq3TR3WgQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.2.0.tgz", + "integrity": "sha512-pzgM1dePPJysVnzaFCMp+BKtjM5q46HZeyShiR+KcQYvneD3fmUPJigDkkcsB2DcrY3mFvDcswjoqxaTIW7ZBQ==", "requires": { - "@sentry/hub": "6.1.0", - "@sentry/minimal": "6.1.0", - "@sentry/types": "6.1.0", - "@sentry/utils": "6.1.0", + "@sentry/hub": "6.2.0", + "@sentry/minimal": "6.2.0", + "@sentry/types": "6.2.0", + "@sentry/utils": "6.2.0", "tslib": "^1.9.3" }, "dependencies": { @@ -282,16 +282,16 @@ } }, "@sentry/types": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.1.0.tgz", - "integrity": "sha512-kIaN52Fw5K+2mKRaHE2YluJ+F/qMGSUzZXIFDNdC6OUMXQ4TM8gZTrITXs8CLDm7cK8iCqFCtzKOjKK6KyOKAg==" + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.0.tgz", + "integrity": "sha512-vN4P/a+QqAuVfWFB9G3nQ7d6bgnM9jd/RLVi49owMuqvM24pv5mTQHUk2Hk4S3k7ConrHFl69E7xH6Dv5VpQnQ==" }, "@sentry/utils": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.1.0.tgz", - "integrity": "sha512-6JAplzUOS6bEwfX0PDRZBbYRvn9EN22kZfcL0qGHtM9L0QQ5ybjbbVwOpbXgRkiZx++dQbzLFtelxnDhsbFG+Q==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.2.0.tgz", + "integrity": "sha512-YToUC7xYf2E/pIluI7upYTlj8fKXOtdwoOBkcQZifHgX/dP+qDaHibbBFe5PyZwdmU2UiLnWFsBr0gjo0QFo1g==", "requires": { - "@sentry/types": "6.1.0", + "@sentry/types": "6.2.0", "tslib": "^1.9.3" }, "dependencies": { diff --git a/web/package.json b/web/package.json index bb2281ae1..2bc6a5727 100644 --- a/web/package.json +++ b/web/package.json @@ -13,7 +13,7 @@ "@fortawesome/fontawesome-free": "^5.15.2", "@patternfly/patternfly": "^4.87.3", "@sentry/browser": "^6.1.0", - "@sentry/tracing": "^6.1.0", + "@sentry/tracing": "^6.2.0", "@types/chart.js": "^2.9.30", "@types/codemirror": "0.0.108", "base64-js": "^1.5.1", From 1e51a2cdd7a911bfa7e227413db34782b114a0c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Feb 2021 09:21:34 +0100 Subject: [PATCH 03/34] build(deps): bump rollup-plugin-copy from 3.3.0 to 3.4.0 in /web (#572) Bumps [rollup-plugin-copy](https://github.com/vladshcherbin/rollup-plugin-copy) from 3.3.0 to 3.4.0. - [Release notes](https://github.com/vladshcherbin/rollup-plugin-copy/releases) - [Commits](https://github.com/vladshcherbin/rollup-plugin-copy/compare/3.3.0...3.4.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package-lock.json | 12 ++++++------ web/package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index ff7bc9fae..8f5554b7a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1682,9 +1682,9 @@ } }, "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, "has-flag": { "version": "3.0.0", @@ -2729,9 +2729,9 @@ } }, "rollup-plugin-copy": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.3.0.tgz", - "integrity": "sha512-euDjCUSBXZa06nqnwCNADbkAcYDfzwowfZQkto9K/TFhiH+QG7I4PUsEMwM9tDgomGWJc//z7KLW8t+tZwxADA==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz", + "integrity": "sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ==", "requires": { "@types/fs-extra": "^8.0.1", "colorette": "^1.1.0", diff --git a/web/package.json b/web/package.json index 2bc6a5727..136e793da 100644 --- a/web/package.json +++ b/web/package.json @@ -24,7 +24,7 @@ "lit-element": "^2.4.0", "lit-html": "^1.3.0", "rollup": "^2.39.0", - "rollup-plugin-copy": "^3.3.0", + "rollup-plugin-copy": "^3.4.0", "rollup-plugin-cssimport": "^1.0.2", "rollup-plugin-external-globals": "^0.6.1", "tslib": "^2.1.0" From 1c919b8b888e2092d912bff172db078cffd7f012 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Feb 2021 09:21:49 +0100 Subject: [PATCH 04/34] build(deps): bump docker from 4.4.2 to 4.4.3 (#570) Bumps [docker](https://github.com/docker/docker-py) from 4.4.2 to 4.4.3. - [Release notes](https://github.com/docker/docker-py/releases) - [Commits](https://github.com/docker/docker-py/compare/4.4.2...4.4.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index d2743b298..ca21b4169 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -124,10 +124,10 @@ }, "botocore": { "hashes": [ - "sha256:8c84eac6daf38890714e005623083106d68e9b2088e62132fdbf7d2b1228ecbd", - "sha256:a601ee5a4ae66832f328ca362b5404d22b75f1c181f6cc0934f3cfca749eb27d" + "sha256:8efd206b78269eb115279ca2d23f50eead1307dbe0bf9bcc2bba3ab2ff7bfd87", + "sha256:dd7c528c6c936d941b2c267339f0b01cce377b640856240b588d0e0d82fd29e3" ], - "version": "==1.20.10" + "version": "==1.20.11" }, "cachetools": { "hashes": [ @@ -409,11 +409,11 @@ }, "docker": { "hashes": [ - "sha256:20d71afc593486f2297bb7fb7406b03876f31894337e914a5062050c65085cab", - "sha256:67f33d4cf95182db631a17eef7d666d2c91f624c1d3fbc4df6009cb2f2a4c604" + "sha256:d4625e70e3d5a12d7cbf1fd68cef2e081ac86b83889e00e5466d975f90e50dad", + "sha256:de5753b7f6486dd541a98393e423e387579b8974a5068748b83f852cc76a89d6" ], "index": "pypi", - "version": "==4.4.2" + "version": "==4.4.3" }, "drf-yasg2": { "hashes": [ From 270be95e684528a7809d3cd229eb94abb50f2468 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Feb 2021 09:22:06 +0100 Subject: [PATCH 05/34] build(deps): bump structlog from 20.2.0 to 21.1.0 (#569) Bumps [structlog](https://github.com/hynek/structlog) from 20.2.0 to 21.1.0. - [Release notes](https://github.com/hynek/structlog/releases) - [Changelog](https://github.com/hynek/structlog/blob/main/CHANGELOG.rst) - [Commits](https://github.com/hynek/structlog/compare/20.2.0...21.1.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index ca21b4169..eb697a124 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1123,11 +1123,11 @@ }, "structlog": { "hashes": [ - "sha256:33dd6bd5f49355e52c1c61bb6a4f20d0b48ce0328cc4a45fe872d38b97a05ccd", - "sha256:af79dfa547d104af8d60f86eac12fb54825f54a46bc998e4504ef66177103174" + "sha256:62f06fc0ee32fb8580f0715eea66cb87271eb7efb0eaf9af6b639cba8981de47", + "sha256:d9d2d890532e8db83c6977a2a676fb1889922ff0c26ad4dc0ecac26f9fafbc57" ], "index": "pypi", - "version": "==20.2.0" + "version": "==21.1.0" }, "swagger-spec-validator": { "hashes": [ From 83b7b3257a91c1c433a7d0b645de2ddf3c395f3f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Feb 2021 09:22:16 +0100 Subject: [PATCH 06/34] build(deps): bump boto3 from 1.17.10 to 1.17.11 (#568) Bumps [boto3](https://github.com/boto/boto3) from 1.17.10 to 1.17.11. - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.17.10...1.17.11) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index eb697a124..f9c8d2ebc 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -116,11 +116,11 @@ }, "boto3": { "hashes": [ - "sha256:1709ff5feb363fee7fcaa2330e659fcbc2b4c03a14f75a884ed682ee66011fc4", - "sha256:80a761eff3b1cb0798d7e1a41b7c8e6d85c9647a8f7b6105335201a69404caa2" + "sha256:7d44cbd931c653cc68e8ccbf39f3ad8b304cb50d4e964d8c8d0936de33ff8c8b", + "sha256:b6131751e3cf2f8d4c027518373b6b82264c3897de65d3519e2d782927e8bf1e" ], "index": "pypi", - "version": "==1.17.10" + "version": "==1.17.11" }, "botocore": { "hashes": [ From c47cef6fbfc708fc59d6389145c1fef4d19a2c7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Feb 2021 09:22:30 +0100 Subject: [PATCH 07/34] build(deps): bump sentry-sdk from 0.20.2 to 0.20.3 (#567) Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 0.20.2 to 0.20.3. - [Release notes](https://github.com/getsentry/sentry-python/releases) - [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-python/compare/0.20.2...0.20.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index f9c8d2ebc..f38d7dcbc 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1093,11 +1093,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:9044b616ec6663cd50794fb3362efd87586e476e7b51ef2439a711d6569aede7", - "sha256:efc65e5ffd38324797a7e1dfc8a4e74b62ea6f56a59df5bb03217b84d58dff6a" + "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237", + "sha256:e75c8c58932bda8cd293ea8e4b242527129e1caaec91433d21b8b2f20fee030b" ], "index": "pypi", - "version": "==0.20.2" + "version": "==0.20.3" }, "service-identity": { "hashes": [ From c23df5e1d526e2c8f1fd12223928627a5d89b813 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Feb 2021 12:16:50 +0100 Subject: [PATCH 08/34] build(deps): bump @sentry/browser from 6.1.0 to 6.2.0 in /web (#571) Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 6.1.0 to 6.2.0. - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/6.1.0...6.2.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package-lock.json | 74 ++++++++----------------------------------- web/package.json | 2 +- 2 files changed, 14 insertions(+), 62 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 8f5554b7a..954ed0718 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -144,30 +144,16 @@ } }, "@sentry/browser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.1.0.tgz", - "integrity": "sha512-t3y2TLXDWgvfknyH8eKj/9mghJfSEqItFyp74zPu1Src6kOPjkd4Sa7o4+bdkNgA8dIIOrDAhRUbB2sq4sWMCA==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.2.0.tgz", + "integrity": "sha512-4r3paHcHXLemj471BtNDhUs2kvJxk5XDRplz1dbC/LHXN5PWEXP4anhGILxOlxqi4y33r53PIZu3xXFjznaVZA==", "requires": { - "@sentry/core": "6.1.0", - "@sentry/types": "6.1.0", - "@sentry/utils": "6.1.0", + "@sentry/core": "6.2.0", + "@sentry/types": "6.2.0", + "@sentry/utils": "6.2.0", "tslib": "^1.9.3" }, "dependencies": { - "@sentry/types": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.1.0.tgz", - "integrity": "sha512-kIaN52Fw5K+2mKRaHE2YluJ+F/qMGSUzZXIFDNdC6OUMXQ4TM8gZTrITXs8CLDm7cK8iCqFCtzKOjKK6KyOKAg==" - }, - "@sentry/utils": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.1.0.tgz", - "integrity": "sha512-6JAplzUOS6bEwfX0PDRZBbYRvn9EN22kZfcL0qGHtM9L0QQ5ybjbbVwOpbXgRkiZx++dQbzLFtelxnDhsbFG+Q==", - "requires": { - "@sentry/types": "6.1.0", - "tslib": "^1.9.3" - } - }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -176,51 +162,17 @@ } }, "@sentry/core": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.1.0.tgz", - "integrity": "sha512-57mXkp3NoyxRycXrL+Ec6bYS6UYJZp9tYX0lUp5Ry2M0FxDZ3Q4drkjr8MIQOhBaQXP2ukSX4QTVLGMPm60zMw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.2.0.tgz", + "integrity": "sha512-oTr2b25l+0bv/+d6IgMamPuGleWV7OgJb0NFfd+WZhw6UDRgr7CdEJy2gW6tK8SerwXgPHdn4ervxsT3WIBiXw==", "requires": { - "@sentry/hub": "6.1.0", - "@sentry/minimal": "6.1.0", - "@sentry/types": "6.1.0", - "@sentry/utils": "6.1.0", + "@sentry/hub": "6.2.0", + "@sentry/minimal": "6.2.0", + "@sentry/types": "6.2.0", + "@sentry/utils": "6.2.0", "tslib": "^1.9.3" }, "dependencies": { - "@sentry/hub": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.1.0.tgz", - "integrity": "sha512-JnBSCgNg3VHiMojUl5tCHU8iWPVuE+qqENIzG9A722oJms1kKWBvWl+yQzhWBNdgk5qeAY3F5UzKWJZkbJ6xow==", - "requires": { - "@sentry/types": "6.1.0", - "@sentry/utils": "6.1.0", - "tslib": "^1.9.3" - } - }, - "@sentry/minimal": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.1.0.tgz", - "integrity": "sha512-g6sfNKenL7wnsr/tibp8nFiMv/XRH0s0Pt4p151npmNI+SmjuUz3GGYEXk8ChCyaKldYKilkNOFdVXJxUf5gZw==", - "requires": { - "@sentry/hub": "6.1.0", - "@sentry/types": "6.1.0", - "tslib": "^1.9.3" - } - }, - "@sentry/types": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.1.0.tgz", - "integrity": "sha512-kIaN52Fw5K+2mKRaHE2YluJ+F/qMGSUzZXIFDNdC6OUMXQ4TM8gZTrITXs8CLDm7cK8iCqFCtzKOjKK6KyOKAg==" - }, - "@sentry/utils": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.1.0.tgz", - "integrity": "sha512-6JAplzUOS6bEwfX0PDRZBbYRvn9EN22kZfcL0qGHtM9L0QQ5ybjbbVwOpbXgRkiZx++dQbzLFtelxnDhsbFG+Q==", - "requires": { - "@sentry/types": "6.1.0", - "tslib": "^1.9.3" - } - }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", diff --git a/web/package.json b/web/package.json index 136e793da..36fa5db79 100644 --- a/web/package.json +++ b/web/package.json @@ -12,7 +12,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.15.2", "@patternfly/patternfly": "^4.87.3", - "@sentry/browser": "^6.1.0", + "@sentry/browser": "^6.2.0", "@sentry/tracing": "^6.2.0", "@types/chart.js": "^2.9.30", "@types/codemirror": "0.0.108", From 1c1f9b6cb8ebc058b26b59534e5f53af37fe6d85 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 15:34:21 +0100 Subject: [PATCH 09/34] web: fix SiteShell not being full height --- web/src/authentik.css | 4 ---- web/src/elements/router/RouterOutlet.ts | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/web/src/authentik.css b/web/src/authentik.css index c052df2bf..7ad666e7f 100644 --- a/web/src/authentik.css +++ b/web/src/authentik.css @@ -85,10 +85,6 @@ select[multiple] { z-index: auto !important; } -.pf-c-page__main { - display: block; -} - @media (prefers-color-scheme: dark) { :root { --ak-dark-foreground: #fafafa; diff --git a/web/src/elements/router/RouterOutlet.ts b/web/src/elements/router/RouterOutlet.ts index e817ab306..5648c1595 100644 --- a/web/src/elements/router/RouterOutlet.ts +++ b/web/src/elements/router/RouterOutlet.ts @@ -65,7 +65,7 @@ export class RouterOutlet extends LitElement { console.debug(`authentik/router: route "${activeUrl}" not defined, defaulting to shell`); const route = new Route( RegExp(""), - html` + html`
` ); From 277b4336d31f7bea2bc3c8e86c4e4bfa2b44675c Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 16:00:59 +0100 Subject: [PATCH 10/34] stages/authenticator_validate: update autocomplete for code input --- authentik/stages/authenticator_validate/forms.py | 2 +- web/src/elements/notifications/NotificationTrigger.ts | 10 ++++++++-- web/src/elements/sidebar/SidebarUser.ts | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/authentik/stages/authenticator_validate/forms.py b/authentik/stages/authenticator_validate/forms.py index 23e87fba0..bdc4c9316 100644 --- a/authentik/stages/authenticator_validate/forms.py +++ b/authentik/stages/authenticator_validate/forms.py @@ -19,7 +19,7 @@ class ValidationForm(forms.Form): label=_("Please enter the token from your device."), widget=forms.TextInput( attrs={ - "autocomplete": "off", + "autocomplete": "one-time-code", "placeholder": "123456", "autofocus": "autofocus", } diff --git a/web/src/elements/notifications/NotificationTrigger.ts b/web/src/elements/notifications/NotificationTrigger.ts index db7f581e0..842a18c2e 100644 --- a/web/src/elements/notifications/NotificationTrigger.ts +++ b/web/src/elements/notifications/NotificationTrigger.ts @@ -1,8 +1,13 @@ -import { customElement, html, LitElement, TemplateResult } from "lit-element"; +import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element"; +import { COMMON_STYLES } from "../../common/styles"; @customElement("ak-notification-trigger") export class NotificationRule extends LitElement { + static get styles(): CSSResult[] { + return COMMON_STYLES; + } + constructor() { super(); this.addEventListener("click", () => { @@ -16,7 +21,8 @@ export class NotificationRule extends LitElement { } render(): TemplateResult { - return html``; + // TODO: Show icon with red dot when unread notifications exist + return html``; } } diff --git a/web/src/elements/sidebar/SidebarUser.ts b/web/src/elements/sidebar/SidebarUser.ts index 73c79f188..01a75d470 100644 --- a/web/src/elements/sidebar/SidebarUser.ts +++ b/web/src/elements/sidebar/SidebarUser.ts @@ -37,11 +37,11 @@ export class SidebarUser extends LitElement { render(): TemplateResult { return html` - ${until(User.me().then(u => { - return html``;}), html``)} + ${until(User.me().then((u) => { + return html``; + }), html``)} - From 6e11fd0f2e7999b2ad61316afcac8c4563e527f9 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 16:13:55 +0100 Subject: [PATCH 11/34] web: fix application library not being full height --- web/src/pages/LibraryPage.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/src/pages/LibraryPage.ts b/web/src/pages/LibraryPage.ts index 19e3f17d8..fa0ae7c7b 100644 --- a/web/src/pages/LibraryPage.ts +++ b/web/src/pages/LibraryPage.ts @@ -14,6 +14,10 @@ export class LibraryApplication extends LitElement { static get styles(): CSSResult[] { return COMMON_STYLES.concat( css` + :host, + main { + height: 100%; + } a { height: 100%; } From cd23053007fae1c3a5374cf31e6ee87b66d6d359 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 16:19:44 +0100 Subject: [PATCH 12/34] web: fix height on table pages --- web/src/elements/router/RouterOutlet.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/src/elements/router/RouterOutlet.ts b/web/src/elements/router/RouterOutlet.ts index 5648c1595..48f5d877e 100644 --- a/web/src/elements/router/RouterOutlet.ts +++ b/web/src/elements/router/RouterOutlet.ts @@ -28,6 +28,11 @@ export class RouterOutlet extends LitElement { :host { height: 100vh; } + *:first-child { + height: 100%; + display: flex; + flex-direction: column; + } `, ].concat(...COMMON_STYLES); } From bd6a473d4f4d02a93285f02465d7da669916af63 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 16:34:12 +0100 Subject: [PATCH 13/34] flows: add cached as action to flow API --- authentik/api/v2/urls.py | 15 ++------------- authentik/core/api/utils.py | 14 +++++++++++++- authentik/flows/api.py | 23 +++++++++++------------ 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index aaa1355c6..87c13e86a 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -23,23 +23,14 @@ from authentik.events.api.event import EventViewSet from authentik.events.api.notification import NotificationViewSet from authentik.events.api.notification_rule import NotificationRuleViewSet from authentik.events.api.notification_transport import NotificationTransportViewSet -from authentik.flows.api import ( - FlowCacheViewSet, - FlowStageBindingViewSet, - FlowViewSet, - StageViewSet, -) +from authentik.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet from authentik.outposts.api.outpost_service_connections import ( DockerServiceConnectionViewSet, KubernetesServiceConnectionViewSet, ServiceConnectionViewSet, ) from authentik.outposts.api.outposts import OutpostViewSet -from authentik.policies.api import ( - PolicyBindingViewSet, - PolicyCacheViewSet, - PolicyViewSet, -) +from authentik.policies.api import PolicyBindingViewSet, PolicyViewSet from authentik.policies.dummy.api import DummyPolicyViewSet from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet @@ -100,7 +91,6 @@ router.register( router.register("outposts/proxy", ProxyOutpostConfigViewSet) router.register("flows/instances", FlowViewSet) -router.register("flows/cached", FlowCacheViewSet, basename="flows_cache") router.register("flows/bindings", FlowStageBindingViewSet) router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet) @@ -116,7 +106,6 @@ router.register("sources/saml", SAMLSourceViewSet) router.register("sources/oauth", OAuthSourceViewSet) router.register("policies/all", PolicyViewSet) -router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache") router.register("policies/bindings", PolicyBindingViewSet) router.register("policies/expression", ExpressionPolicyViewSet) router.register("policies/event_matcher", EventMatcherPolicyViewSet) diff --git a/authentik/core/api/utils.py b/authentik/core/api/utils.py index be583bdcc..b93f75155 100644 --- a/authentik/core/api/utils.py +++ b/authentik/core/api/utils.py @@ -1,6 +1,6 @@ """API Utilities""" from django.db.models import Model -from rest_framework.fields import CharField +from rest_framework.fields import CharField, IntegerField from rest_framework.serializers import Serializer, SerializerMethodField @@ -37,3 +37,15 @@ class TypeCreateSerializer(Serializer): def update(self, instance: Model, validated_data: dict) -> Model: raise NotImplementedError + + +class CacheSerializer(Serializer): + """Generic cache stats for an object""" + + count = IntegerField(read_only=True) + + def create(self, validated_data: dict) -> Model: + raise NotImplementedError + + def update(self, instance: Model, validated_data: dict) -> Model: + raise NotImplementedError diff --git a/authentik/flows/api.py b/authentik/flows/api.py index dc66c5414..03b84ad69 100644 --- a/authentik/flows/api.py +++ b/authentik/flows/api.py @@ -18,7 +18,11 @@ from rest_framework.serializers import ( ) from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet -from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer +from authentik.core.api.utils import ( + CacheSerializer, + MetaNameSerializer, + TypeCreateSerializer, +) from authentik.flows.models import Flow, FlowStageBinding, Stage from authentik.flows.planner import cache_key from authentik.lib.templatetags.authentik_utils import verbose_name @@ -84,6 +88,12 @@ class FlowViewSet(ModelViewSet): search_fields = ["name", "slug", "designation", "title"] filterset_fields = ["flow_uuid", "name", "slug", "designation"] + @swagger_auto_schema(responses={200: CacheSerializer(many=False)}) + @action(detail=False) + def cached(self, request: Request) -> Response: + """Info about cached flows""" + return Response(data={"count": len(cache.keys("flow_*"))}) + @swagger_auto_schema(responses={200: FlowDiagramSerializer()}) @action(detail=True, methods=["get"]) def diagram(self, request: Request, slug: str) -> Response: @@ -226,14 +236,3 @@ class FlowStageBindingViewSet(ModelViewSet): queryset = FlowStageBinding.objects.all() serializer_class = FlowStageBindingSerializer filterset_fields = "__all__" - - -class FlowCacheViewSet(ListModelMixin, GenericViewSet): - """Info about cached flows""" - - queryset = Flow.objects.none() - serializer_class = Serializer - - def list(self, request: Request) -> Response: - """Info about cached flows""" - return Response(data={"pagination": {"count": len(cache.keys("flow_*"))}}) From 47bde052caf9f922a1cd04f78ea0b26fd8dd64c8 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 16:34:33 +0100 Subject: [PATCH 14/34] policies: add types action to policy API, use MetaNameSerializer --- authentik/policies/api.py | 60 ++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/authentik/policies/api.py b/authentik/policies/api.py index 7f179d330..2f271719a 100644 --- a/authentik/policies/api.py +++ b/authentik/policies/api.py @@ -1,17 +1,25 @@ """policy API Views""" from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist -from rest_framework.mixins import ListModelMixin +from django.shortcuts import reverse +from drf_yasg2.utils import swagger_auto_schema +from rest_framework.decorators import action from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ( ModelSerializer, PrimaryKeyRelatedField, - Serializer, SerializerMethodField, ) -from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet +from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet +from authentik.core.api.utils import ( + CacheSerializer, + MetaNameSerializer, + TypeCreateSerializer, +) +from authentik.lib.templatetags.authentik_utils import verbose_name +from authentik.lib.utils.reflection import all_subclasses from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel @@ -45,7 +53,7 @@ class PolicyBindingModelForeignKey(PrimaryKeyRelatedField): return correct_model.pk -class PolicySerializer(ModelSerializer): +class PolicySerializer(ModelSerializer, MetaNameSerializer): """Policy Serializer""" _resolve_inheritance: bool @@ -58,7 +66,7 @@ class PolicySerializer(ModelSerializer): def get_object_type(self, obj): """Get object type so that we know which API Endpoint to use to get the full object""" - return obj._meta.object_name.lower().replace("provider", "") + return obj._meta.object_name.lower().replace("policy", "") def to_representation(self, instance: Policy): # pyright: reportGeneralTypeIssues=false @@ -71,7 +79,14 @@ class PolicySerializer(ModelSerializer): class Meta: model = Policy - fields = ["pk", "name", "execution_logging", "object_type"] + fields = [ + "pk", + "name", + "execution_logging", + "object_type", + "verbose_name", + "verbose_name_plural", + ] depth = 3 @@ -88,6 +103,28 @@ class PolicyViewSet(ReadOnlyModelViewSet): def get_queryset(self): return Policy.objects.select_subclasses() + @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)}) + @action(detail=False) + def types(self, request: Request) -> Response: + """Get all creatable policy types""" + data = [] + for subclass in all_subclasses(self.queryset.model): + data.append( + { + "name": verbose_name(subclass), + "description": subclass.__doc__, + "link": reverse("authentik_admin:policy-create") + + f"?type={subclass.__name__}", + } + ) + return Response(TypeCreateSerializer(data, many=True).data) + + @swagger_auto_schema(responses={200: CacheSerializer(many=False)}) + @action(detail=False) + def cached(self, request: Request) -> Response: + """Info about cached policies""" + return Response(data={"count": len(cache.keys("policy_*"))}) + class PolicyBindingSerializer(ModelSerializer): """PolicyBinding Serializer""" @@ -124,14 +161,3 @@ class PolicyBindingViewSet(ModelViewSet): serializer_class = PolicyBindingSerializer filterset_fields = ["policy", "target", "enabled", "order", "timeout"] search_fields = ["policy__name"] - - -class PolicyCacheViewSet(ListModelMixin, GenericViewSet): - """Info about cached policies""" - - queryset = Policy.objects.none() - serializer_class = Serializer - - def list(self, request: Request) -> Response: - """Info about cached policies""" - return Response(data={"pagination": {"count": len(cache.keys("policy_*"))}}) From 44e51970e13ff99ddacd36b5ff756de91de99992 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 16:34:43 +0100 Subject: [PATCH 15/34] web: update for new cached actions --- swagger.yaml | 366 ++++++++++++++++++++++++++-------------- web/src/api/Flows.ts | 4 +- web/src/api/Policies.ts | 4 +- 3 files changed, 245 insertions(+), 129 deletions(-) diff --git a/swagger.yaml b/swagger.yaml index e6b8005c1..ebd5de26e 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -1600,59 +1600,6 @@ paths: required: true type: string format: uuid - /flows/cached/: - get: - operationId: flows_cached_list - description: Info about cached flows - parameters: - - name: ordering - in: query - description: Which field to use when ordering the results. - required: false - type: string - - name: search - in: query - description: A search term. - required: false - type: string - - name: page - in: query - description: A page number within the paginated result set. - required: false - type: integer - - name: page_size - in: query - description: Number of results to return per page. - required: false - type: integer - responses: - '200': - description: '' - schema: - required: - - count - - results - type: object - properties: - count: - type: integer - next: - type: string - format: uri - x-nullable: true - previous: - type: string - format: uri - x-nullable: true - results: - type: array - items: - description: '' - type: object - properties: {} - tags: - - flows - parameters: [] /flows/instances/: get: operationId: flows_instances_list @@ -1740,6 +1687,59 @@ paths: tags: - flows parameters: [] + /flows/instances/cached/: + get: + operationId: flows_instances_cached + description: Info about cached flows + parameters: + - name: flow_uuid + in: query + description: '' + required: false + type: string + - name: name + in: query + description: '' + required: false + type: string + - name: slug + in: query + description: '' + required: false + type: string + - name: designation + in: query + description: '' + required: false + type: string + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: Generic cache stats for an object + schema: + $ref: '#/definitions/Cache' + tags: + - flows + parameters: [] /flows/instances/{slug}/: get: operationId: flows_instances_read @@ -2544,6 +2544,95 @@ paths: tags: - policies parameters: [] + /policies/all/cached/: + get: + operationId: policies_all_cached + description: Info about cached policies + parameters: + - name: bindings__isnull + in: query + description: '' + required: false + type: string + - name: promptstage__isnull + in: query + description: '' + required: false + type: string + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: Generic cache stats for an object + schema: + $ref: '#/definitions/Cache' + tags: + - policies + parameters: [] + /policies/all/types/: + get: + operationId: policies_all_types + description: Get all creatable policy types + parameters: + - name: bindings__isnull + in: query + description: '' + required: false + type: string + - name: promptstage__isnull + in: query + description: '' + required: false + type: string + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: Types of an object that can be created + schema: + description: '' + type: array + items: + $ref: '#/definitions/TypeCreate' + tags: + - policies + parameters: [] /policies/all/{policy_uuid}/: get: operationId: policies_all_read @@ -2715,59 +2804,6 @@ paths: required: true type: string format: uuid - /policies/cached/: - get: - operationId: policies_cached_list - description: Info about cached policies - parameters: - - name: ordering - in: query - description: Which field to use when ordering the results. - required: false - type: string - - name: search - in: query - description: A search term. - required: false - type: string - - name: page - in: query - description: A page number within the paginated result set. - required: false - type: integer - - name: page_size - in: query - description: Number of results to return per page. - required: false - type: integer - responses: - '200': - description: '' - schema: - required: - - count - - results - type: object - properties: - count: - type: integer - next: - type: string - format: uri - x-nullable: true - previous: - type: string - format: uri - x-nullable: true - results: - type: array - items: - description: '' - type: object - properties: {} - tags: - - policies - parameters: [] /policies/dummy/: get: operationId: policies_dummy_list @@ -8486,6 +8522,14 @@ definitions: title: Cache count type: string readOnly: true + Cache: + description: Generic cache stats for an object + type: object + properties: + count: + title: Count + type: integer + readOnly: true FlowDiagram: description: response of the flow's /diagram/ action type: object @@ -8801,6 +8845,33 @@ definitions: title: Object type type: string readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true + TypeCreate: + description: Types of an object that can be created + type: object + properties: + name: + title: Name + type: string + readOnly: true + minLength: 1 + description: + title: Description + type: string + readOnly: true + minLength: 1 + link: + title: Link + type: string + readOnly: true + minLength: 1 PolicyBinding: description: PolicyBinding Serializer required: @@ -9175,6 +9246,14 @@ definitions: title: Object type type: string readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true result: title: Result type: boolean @@ -9210,6 +9289,14 @@ definitions: title: Object type type: string readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true action: title: Action description: Match created events with this action type. When left empty, @@ -9315,6 +9402,14 @@ definitions: title: Object type type: string readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true expression: title: Expression type: string @@ -9341,6 +9436,14 @@ definitions: title: Object type type: string readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true group: title: Group type: string @@ -9368,6 +9471,14 @@ definitions: title: Object type type: string readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true password_field: title: Password field description: Field key to check, field keys defined in Prompt stages are available. @@ -9402,6 +9513,14 @@ definitions: title: Object type type: string readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true password_field: title: Password field description: Field key to check, field keys defined in Prompt stages are available. @@ -9459,6 +9578,14 @@ definitions: title: Object type type: string readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true days: title: Days type: integer @@ -9489,6 +9616,14 @@ definitions: title: Object type type: string readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true check_ip: title: Check ip type: boolean @@ -9641,25 +9776,6 @@ definitions: title: Verbose name plural type: string readOnly: true - TypeCreate: - description: Types of an object that can be created - type: object - properties: - name: - title: Name - type: string - readOnly: true - minLength: 1 - description: - title: Description - type: string - readOnly: true - minLength: 1 - link: - title: Link - type: string - readOnly: true - minLength: 1 OAuth2Provider: description: OAuth2Provider Serializer required: diff --git a/web/src/api/Flows.ts b/web/src/api/Flows.ts index 18f604e2b..1b1af9825 100644 --- a/web/src/api/Flows.ts +++ b/web/src/api/Flows.ts @@ -40,8 +40,8 @@ export class Flow { } static cached(): Promise { - return DefaultClient.fetch>(["flows", "cached"]).then(r => { - return r.pagination.count; + return DefaultClient.fetch<{ count: number }>(["flows", "all", "cached"]).then(r => { + return r.count; }); } static adminUrl(rest: string): string { diff --git a/web/src/api/Policies.ts b/web/src/api/Policies.ts index 7e5d78eeb..120c7f839 100644 --- a/web/src/api/Policies.ts +++ b/web/src/api/Policies.ts @@ -20,8 +20,8 @@ export class Policy implements BaseInheritanceModel { } static cached(): Promise { - return DefaultClient.fetch>(["policies", "cached"]).then(r => { - return r.pagination.count; + return DefaultClient.fetch<{ count: number }>(["policies", "all", "cached"]).then(r => { + return r.count; }); } } From 79089d8981ff17ea740a2ed4f8a7e21f9275e911 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 16:53:30 +0100 Subject: [PATCH 16/34] policies: add bound count to api --- authentik/policies/api.py | 14 ++++++++++++-- swagger.yaml | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/authentik/policies/api.py b/authentik/policies/api.py index 2f271719a..703d4bf0f 100644 --- a/authentik/policies/api.py +++ b/authentik/policies/api.py @@ -59,15 +59,22 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer): _resolve_inheritance: bool object_type = SerializerMethodField() + bound_to = SerializerMethodField() def __init__(self, *args, resolve_inheritance: bool = True, **kwargs): super().__init__(*args, **kwargs) self._resolve_inheritance = resolve_inheritance - def get_object_type(self, obj): + def get_object_type(self, obj: Policy) -> str: """Get object type so that we know which API Endpoint to use to get the full object""" return obj._meta.object_name.lower().replace("policy", "") + def get_bound_to(self, obj: Policy) -> int: + """Return objects policy is bound to""" + if not obj.bindings.exists() and not obj.promptstage_set.exists(): + return 0 + return obj.bindings.count() + def to_representation(self, instance: Policy): # pyright: reportGeneralTypeIssues=false if instance.__class__ == Policy or not self._resolve_inheritance: @@ -86,6 +93,7 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer): "object_type", "verbose_name", "verbose_name_plural", + "bound_to", ] depth = 3 @@ -101,7 +109,9 @@ class PolicyViewSet(ReadOnlyModelViewSet): } def get_queryset(self): - return Policy.objects.select_subclasses() + return Policy.objects.select_subclasses().prefetch_related( + "bindings", "promptstage_set" + ) @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)}) @action(detail=False) diff --git a/swagger.yaml b/swagger.yaml index ebd5de26e..99d8fb2fc 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -8853,6 +8853,10 @@ definitions: title: Verbose name plural type: string readOnly: true + bound_to: + title: Bound to + type: integer + readOnly: true TypeCreate: description: Types of an object that can be created type: object @@ -9254,6 +9258,10 @@ definitions: title: Verbose name plural type: string readOnly: true + bound_to: + title: Bound to + type: integer + readOnly: true result: title: Result type: boolean @@ -9297,6 +9305,10 @@ definitions: title: Verbose name plural type: string readOnly: true + bound_to: + title: Bound to + type: integer + readOnly: true action: title: Action description: Match created events with this action type. When left empty, @@ -9410,6 +9422,10 @@ definitions: title: Verbose name plural type: string readOnly: true + bound_to: + title: Bound to + type: integer + readOnly: true expression: title: Expression type: string @@ -9444,6 +9460,10 @@ definitions: title: Verbose name plural type: string readOnly: true + bound_to: + title: Bound to + type: integer + readOnly: true group: title: Group type: string @@ -9479,6 +9499,10 @@ definitions: title: Verbose name plural type: string readOnly: true + bound_to: + title: Bound to + type: integer + readOnly: true password_field: title: Password field description: Field key to check, field keys defined in Prompt stages are available. @@ -9521,6 +9545,10 @@ definitions: title: Verbose name plural type: string readOnly: true + bound_to: + title: Bound to + type: integer + readOnly: true password_field: title: Password field description: Field key to check, field keys defined in Prompt stages are available. @@ -9586,6 +9614,10 @@ definitions: title: Verbose name plural type: string readOnly: true + bound_to: + title: Bound to + type: integer + readOnly: true days: title: Days type: integer @@ -9624,6 +9656,10 @@ definitions: title: Verbose name plural type: string readOnly: true + bound_to: + title: Bound to + type: integer + readOnly: true check_ip: title: Check ip type: boolean From 38bd05867d295fc6b1eaa3ba127cc2104790d8a4 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 17:05:02 +0100 Subject: [PATCH 17/34] web: migrate Policy list to web --- .../templates/administration/policy/list.html | 151 ------------------ authentik/admin/urls.py | 1 - authentik/admin/views/policies.py | 28 +--- web/src/api/Policies.ts | 17 +- web/src/interfaces/AdminInterface.ts | 2 +- web/src/pages/policies/PolicyListPage.ts | 108 +++++++++++++ web/src/routes.ts | 22 +-- 7 files changed, 139 insertions(+), 190 deletions(-) delete mode 100644 authentik/admin/templates/administration/policy/list.html create mode 100644 web/src/pages/policies/PolicyListPage.ts diff --git a/authentik/admin/templates/administration/policy/list.html b/authentik/admin/templates/administration/policy/list.html deleted file mode 100644 index 3b4d35a9a..000000000 --- a/authentik/admin/templates/administration/policy/list.html +++ /dev/null @@ -1,151 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load authentik_utils %} - -{% block content %} -
-
-

- - {% trans 'Policies' %} -

-

{% trans "Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Stages." %}

-
-
-
-
- {% if object_list %} -
-
- {% include 'partials/toolbar_search.html' %} -
- - - - - -
- {% include 'partials/pagination.html' %} -
-
- - - - - - - - - - {% for policy in object_list %} - - - - - - {% endfor %} - -
{% trans 'Name' %}{% trans 'Type' %}
-
-
{{ policy.name }}
- {% if not policy.bindings.exists and not policy.promptstage_set.exists %} - - {% trans 'Warning: Policy is not assigned.' %} - {% else %} - - {% blocktrans with object_count=policy.bindings.all|length %}Assigned to {{ object_count }} objects.{% endblocktrans %} - {% endif %} -
-
- - {{ policy|verbose_name }} - - - - - {% trans 'Edit' %} - -
-
- - - {% trans 'Test' %} - -
-
- - - {% trans 'Delete' %} - -
-
-
-
- {% include 'partials/pagination.html' %} -
- {% else %} -
-
- {% include 'partials/toolbar_search.html' %} -
-
-
-
- -

- {% trans 'No Policies.' %} -

-
- {% if request.GET.search != "" %} - {% trans "Your search query doesn't match any policies." %} - {% else %} - {% trans 'Currently no policies exist. Click the button below to create one.' %} - {% endif %} -
- - - - -
-
- {% endif %} -
-
-{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index c35cdbc90..b0f0aece6 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -73,7 +73,6 @@ urlpatterns = [ name="source-delete", ), # Policies - path("policies/", policies.PolicyListView.as_view(), name="policies"), path("policies/create/", policies.PolicyCreateView.as_view(), name="policy-create"), path( "policies//update/", diff --git a/authentik/admin/views/policies.py b/authentik/admin/views/policies.py index 3a999f3cc..d4491edf2 100644 --- a/authentik/admin/views/policies.py +++ b/authentik/admin/views/policies.py @@ -7,42 +7,22 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.http import HttpResponse -from django.urls import reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import FormView from django.views.generic.detail import DetailView -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from guardian.mixins import PermissionRequiredMixin from authentik.admin.forms.policies import PolicyTestForm from authentik.admin.views.utils import ( BackSuccessUrlMixin, DeleteMessageView, InheritanceCreateView, - InheritanceListView, InheritanceUpdateView, - SearchListMixin, - UserPaginateListMixin, ) from authentik.policies.models import Policy, PolicyBinding from authentik.policies.process import PolicyProcess, PolicyRequest -class PolicyListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - InheritanceListView, -): - """Show list of all policies""" - - model = Policy - permission_required = "authentik_policies.view_policy" - ordering = "name" - template_name = "administration/policy/list.html" - search_fields = ["name"] - - class PolicyCreateView( SuccessMessageMixin, BackSuccessUrlMixin, @@ -56,7 +36,7 @@ class PolicyCreateView( permission_required = "authentik_policies.add_policy" template_name = "generic/create.html" - success_url = reverse_lazy("authentik_admin:policies") + success_url = "/" success_message = _("Successfully created Policy") @@ -73,7 +53,7 @@ class PolicyUpdateView( permission_required = "authentik_policies.change_policy" template_name = "generic/update.html" - success_url = reverse_lazy("authentik_admin:policies") + success_url = "/" success_message = _("Successfully updated Policy") @@ -84,7 +64,7 @@ class PolicyDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessag permission_required = "authentik_policies.delete_policy" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:policies") + success_url = "/" success_message = _("Successfully deleted Policy") diff --git a/web/src/api/Policies.ts b/web/src/api/Policies.ts index 120c7f839..d720bd559 100644 --- a/web/src/api/Policies.ts +++ b/web/src/api/Policies.ts @@ -1,15 +1,18 @@ import { DefaultClient, BaseInheritanceModel, AKResponse, QueryArguments } from "./Client"; +import { TypeCreate } from "./Providers"; export class Policy implements BaseInheritanceModel { pk: string; name: string; + execution_logging: boolean; + object_type: string; + verbose_name: string; + verbose_name_plural: string; + bound_to: number; constructor() { throw Error(); } - object_type: string; - verbose_name: string; - verbose_name_plural: string; static get(pk: string): Promise { return DefaultClient.fetch(["policies", "all", pk]); @@ -24,4 +27,12 @@ export class Policy implements BaseInheritanceModel { return r.count; }); } + + static getTypes(): Promise { + return DefaultClient.fetch(["policies", "all", "types"]); + } + + static adminUrl(rest: string): string { + return `/administration/policies/${rest}`; + } } diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index 57f3e3329..a34ff9170 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -33,7 +33,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ return User.me().then(u => u.is_superuser); }), new SidebarItem("Customisation").children( - new SidebarItem("Policies", "/administration/policies/"), + new SidebarItem("Policies", "/policies"), new SidebarItem("Property Mappings", "/property-mappings"), ).when((): Promise => { return User.me().then(u => u.is_superuser); diff --git a/web/src/pages/policies/PolicyListPage.ts b/web/src/pages/policies/PolicyListPage.ts new file mode 100644 index 000000000..503e2c660 --- /dev/null +++ b/web/src/pages/policies/PolicyListPage.ts @@ -0,0 +1,108 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { AKResponse } from "../../api/Client"; +import { TablePage } from "../../elements/table/TablePage"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/Dropdown"; +import "../../elements/buttons/SpinnerButton"; +import { TableColumn } from "../../elements/table/Table"; +import { Policy } from "../../api/Policies"; +import { until } from "lit-html/directives/until"; + +@customElement("ak-policy-list") +export class PolicyListPage extends TablePage { + searchEnabled(): boolean { + return true; + } + pageTitle(): string { + return gettext("Policies"); + } + pageDescription(): string { + return gettext("Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Stages."); + } + pageIcon(): string { + return gettext("pf-icon pf-icon-infrastructure"); + } + + @property() + order = "name"; + + apiEndpoint(page: number): Promise> { + return Policy.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn("Name", "name"), + new TableColumn("Type"), + new TableColumn(""), + ]; + } + + row(item: Policy): TemplateResult[] { + return [ + html`
+
${item.name}
+ ${item.bound_to > 0 ? + html` + + ${gettext(`Assigned to ${item.bound_to} objects.`)} + `: + html` + ${gettext("Warning: Policy is not assigned.")}/small>`} +
`, + html`${item.verbose_name}`, + html` + + + ${gettext("Edit")} + +
+
+ + + ${gettext("Test")} + +
+
+ + + ${gettext("Delete")} + +
+
+ `, + ]; + } + + renderToolbar(): TemplateResult { + return html` + + + + + ${super.renderToolbar()}`; + } + +} diff --git a/web/src/routes.ts b/web/src/routes.ts index 778b7e4d1..63e114b96 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -1,23 +1,24 @@ import { html } from "lit-html"; import { Route, SLUG_REGEX, ID_REGEX, UUID_REGEX } from "./elements/router/Route"; -import "./pages/LibraryPage"; import "./pages/admin-overview/AdminOverviewPage"; import "./pages/applications/ApplicationListPage"; import "./pages/applications/ApplicationViewPage"; -import "./pages/sources/SourcesListPage"; -import "./pages/sources/SourceViewPage"; +import "./pages/crypto/CertificateKeyPairListPage"; +import "./pages/events/EventInfoPage"; +import "./pages/events/EventListPage"; +import "./pages/events/RuleListPage"; +import "./pages/events/TransportListPage"; import "./pages/flows/FlowListPage"; import "./pages/flows/FlowViewPage"; -import "./pages/events/EventListPage"; -import "./pages/events/EventInfoPage"; -import "./pages/events/TransportListPage"; -import "./pages/events/RuleListPage"; +import "./pages/LibraryPage"; +import "./pages/outposts/OutpostListPage"; +import "./pages/policies/PolicyListPage"; +import "./pages/property-mappings/PropertyMappingListPage"; import "./pages/providers/ProviderListPage"; import "./pages/providers/ProviderViewPage"; -import "./pages/property-mappings/PropertyMappingListPage"; -import "./pages/outposts/OutpostListPage"; -import "./pages/crypto/CertificateKeyPairListPage"; +import "./pages/sources/SourcesListPage"; +import "./pages/sources/SourceViewPage"; export const ROUTES: Route[] = [ // Prevent infinite Shell loops @@ -37,6 +38,7 @@ export const ROUTES: Route[] = [ new Route(new RegExp(`^/sources/(?${SLUG_REGEX})$`)).then((args) => { return html``; }), + new Route(new RegExp("^/policies$"), html``), new Route(new RegExp("^/flows$"), html``), new Route(new RegExp(`^/flows/(?${SLUG_REGEX})$`)).then((args) => { return html``; From 0993d5ce4a4be97e0a2064a64374356e284cbbb1 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 17:10:16 +0100 Subject: [PATCH 18/34] web: reset retryDelay for WS connection after successful connect --- web/src/elements/messages/MessageContainer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/elements/messages/MessageContainer.ts b/web/src/elements/messages/MessageContainer.ts index 4545a9f74..319640cf8 100644 --- a/web/src/elements/messages/MessageContainer.ts +++ b/web/src/elements/messages/MessageContainer.ts @@ -47,6 +47,7 @@ export class MessageContainer extends LitElement { this.messageSocket = new WebSocket(wsUrl); this.messageSocket.addEventListener("open", () => { console.debug(`authentik/messages: connected to ${wsUrl}`); + this.retryDelay = 200; }); this.messageSocket.addEventListener("close", (e) => { console.debug(`authentik/messages: closed ws connection: ${e}`); From 71f771c22cf134747978372beb8c18e04f4f1d80 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 17:10:30 +0100 Subject: [PATCH 19/34] core: add types API to propertymapping --- authentik/core/api/propertymappings.py | 25 ++++++++++- swagger.yaml | 41 ++++++++++++++++++ web/src/api/PropertyMapping.ts | 5 +++ .../PropertyMappingListPage.ts | 43 ++++++------------- 4 files changed, 83 insertions(+), 31 deletions(-) diff --git a/authentik/core/api/propertymappings.py b/authentik/core/api/propertymappings.py index b87865c86..147ba44fe 100644 --- a/authentik/core/api/propertymappings.py +++ b/authentik/core/api/propertymappings.py @@ -1,9 +1,16 @@ """PropertyMapping API Views""" +from django.shortcuts import reverse +from drf_yasg2.utils import swagger_auto_schema +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response from rest_framework.serializers import ModelSerializer, SerializerMethodField from rest_framework.viewsets import ReadOnlyModelViewSet -from authentik.core.api.utils import MetaNameSerializer +from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer from authentik.core.models import PropertyMapping +from authentik.lib.templatetags.authentik_utils import verbose_name +from authentik.lib.utils.reflection import all_subclasses class PropertyMappingSerializer(ModelSerializer, MetaNameSerializer): @@ -47,3 +54,19 @@ class PropertyMappingViewSet(ReadOnlyModelViewSet): def get_queryset(self): return PropertyMapping.objects.select_subclasses() + + @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)}) + @action(detail=False) + def types(self, request: Request) -> Response: + """Get all creatable property-mapping types""" + data = [] + for subclass in all_subclasses(self.queryset.model): + data.append( + { + "name": verbose_name(subclass), + "description": subclass.__doc__, + "link": reverse("authentik_admin:property-mapping-create") + + f"?type={subclass.__name__}", + } + ) + return Response(TypeCreateSerializer(data, many=True).data) diff --git a/swagger.yaml b/swagger.yaml index 99d8fb2fc..3274888bc 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -3876,6 +3876,47 @@ paths: tags: - propertymappings parameters: [] + /propertymappings/all/types/: + get: + operationId: propertymappings_all_types + description: Get all creatable property-mapping types + parameters: + - name: managed__isnull + in: query + description: '' + required: false + type: string + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: Types of an object that can be created + schema: + description: '' + type: array + items: + $ref: '#/definitions/TypeCreate' + tags: + - propertymappings + parameters: [] /propertymappings/all/{pm_uuid}/: get: operationId: propertymappings_all_read diff --git a/web/src/api/PropertyMapping.ts b/web/src/api/PropertyMapping.ts index 1a61133f5..eae1f176f 100644 --- a/web/src/api/PropertyMapping.ts +++ b/web/src/api/PropertyMapping.ts @@ -1,4 +1,5 @@ import { DefaultClient, AKResponse, QueryArguments } from "./Client"; +import { TypeCreate } from "./Providers"; export class PropertyMapping { pk: string; @@ -20,6 +21,10 @@ export class PropertyMapping { return DefaultClient.fetch>(["propertymappings", "all"], filter); } + static getTypes(): Promise { + return DefaultClient.fetch(["propertymappings", "all", "types"]); + } + static adminUrl(rest: string): string { return `/administration/property-mappings/${rest}`; } diff --git a/web/src/pages/property-mappings/PropertyMappingListPage.ts b/web/src/pages/property-mappings/PropertyMappingListPage.ts index 7fa245f5d..210059ce1 100644 --- a/web/src/pages/property-mappings/PropertyMappingListPage.ts +++ b/web/src/pages/property-mappings/PropertyMappingListPage.ts @@ -8,6 +8,7 @@ import "../../elements/buttons/ModalButton"; import "../../elements/buttons/Dropdown"; import "../../elements/buttons/SpinnerButton"; import { TableColumn } from "../../elements/table/Table"; +import { until } from "lit-html/directives/until"; @customElement("ak-property-mapping-list") export class PropertyMappingListPage extends TablePage { @@ -82,36 +83,18 @@ export class PropertyMappingListPage extends TablePage { ${super.renderToolbar()}`; From 029c6cd182bb53c17daa36d18b04d0d82e198d32 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 17:18:09 +0100 Subject: [PATCH 20/34] web: migrate Group list to web --- .../templates/administration/group/list.html | 114 ------------------ authentik/admin/urls.py | 1 - authentik/admin/views/groups.py | 34 +----- web/src/api/Groups.ts | 15 ++- web/src/interfaces/AdminInterface.ts | 2 +- web/src/pages/groups/GroupListPage.ts | 80 ++++++++++++ web/src/routes.ts | 2 + 7 files changed, 103 insertions(+), 145 deletions(-) delete mode 100644 authentik/admin/templates/administration/group/list.html create mode 100644 web/src/pages/groups/GroupListPage.ts diff --git a/authentik/admin/templates/administration/group/list.html b/authentik/admin/templates/administration/group/list.html deleted file mode 100644 index 3d1b8eb0f..000000000 --- a/authentik/admin/templates/administration/group/list.html +++ /dev/null @@ -1,114 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} - -{% block content %} -
-
-

- - {% trans 'Groups' %} -

-

{% trans "Group users together and give them permissions based on the membership." %} -

-
-
-
-
- {% if object_list %} -
-
- {% include 'partials/toolbar_search.html' %} -
- - - {% trans 'Create' %} - -
-
- -
- {% include 'partials/pagination.html' %} -
-
- - - - - - - - - - - {% for group in object_list %} - - - - - - - {% endfor %} - -
{% trans 'Name' %}{% trans 'Parent' %}{% trans 'Members' %}
- - {{ group.name }} - - - - {{ group.parent }} - - - - {{ group.users.all|length }} - - - - - {% trans 'Edit' %} - -
-
- - - {% trans 'Delete' %} - -
-
-
-
- {% include 'partials/pagination.html' %} -
- {% else %} -
-
- {% include 'partials/toolbar_search.html' %} -
-
-
-
- -

- {% trans 'No Groups.' %} -

-
- {% if request.GET.search != "" %} - {% trans "Your search query doesn't match any groups." %} - {% else %} - {% trans 'Currently no group exist. Click the button below to create one.' %} - {% endif %} -
- - - {% trans 'Create' %} - -
-
-
-
- {% endif %} -
-
-{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index b0f0aece6..d2deda6e9 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -269,7 +269,6 @@ urlpatterns = [ name="user-password-reset", ), # Groups - path("groups/", groups.GroupListView.as_view(), name="groups"), path("groups/create/", groups.GroupCreateView.as_view(), name="group-create"), path( "groups//update/", diff --git a/authentik/admin/views/groups.py b/authentik/admin/views/groups.py index bebd3bdb1..462fd87ba 100644 --- a/authentik/admin/views/groups.py +++ b/authentik/admin/views/groups.py @@ -4,38 +4,16 @@ from django.contrib.auth.mixins import ( PermissionRequiredMixin as DjangoPermissionRequiredMixin, ) from django.contrib.messages.views import SuccessMessageMixin -from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from django.views.generic import ListView, UpdateView -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from django.views.generic import UpdateView +from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import ( - BackSuccessUrlMixin, - DeleteMessageView, - SearchListMixin, - UserPaginateListMixin, -) +from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView from authentik.core.forms.groups import GroupForm from authentik.core.models import Group from authentik.lib.views import CreateAssignPermView -class GroupListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - ListView, -): - """Show list of all groups""" - - model = Group - permission_required = "authentik_core.view_group" - ordering = "name" - template_name = "administration/group/list.html" - search_fields = ["name", "attributes"] - - class GroupCreateView( SuccessMessageMixin, BackSuccessUrlMixin, @@ -50,7 +28,7 @@ class GroupCreateView( permission_required = "authentik_core.add_group" template_name = "generic/create.html" - success_url = reverse_lazy("authentik_admin:groups") + success_url = "/" success_message = _("Successfully created Group") @@ -68,7 +46,7 @@ class GroupUpdateView( permission_required = "authentik_core.change_group" template_name = "generic/update.html" - success_url = reverse_lazy("authentik_admin:groups") + success_url = "/" success_message = _("Successfully updated Group") @@ -79,5 +57,5 @@ class GroupDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessage permission_required = "authentik_flows.delete_group" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:groups") + success_url = "/" success_message = _("Successfully deleted Group") diff --git a/web/src/api/Groups.ts b/web/src/api/Groups.ts index 36b73b48a..1d85c05b0 100644 --- a/web/src/api/Groups.ts +++ b/web/src/api/Groups.ts @@ -1,15 +1,28 @@ +import { DefaultClient, QueryArguments, AKResponse } from "./Client"; import { EventContext } from "./Events"; export class Group { - group_uuid: string; + pk: string; name: string; is_superuser: boolean; attributes: EventContext; parent?: Group; + users: number[]; constructor() { throw Error(); } + static get(pk: string): Promise { + return DefaultClient.fetch(["core", "groups", pk]); + } + + static list(filter?: QueryArguments): Promise> { + return DefaultClient.fetch>(["core", "groups"], filter); + } + + static adminUrl(rest: string): string { + return `/administration/groups/${rest}`; + } } diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index a34ff9170..75d791835 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -48,7 +48,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ }), new SidebarItem("Identity & Cryptography").children( new SidebarItem("User", "/administration/users/"), - new SidebarItem("Groups", "/administration/groups/"), + new SidebarItem("Groups", "/groups"), new SidebarItem("Certificates", "/crypto/certificates"), new SidebarItem("Tokens", "/administration/tokens/"), ).when((): Promise => { diff --git a/web/src/pages/groups/GroupListPage.ts b/web/src/pages/groups/GroupListPage.ts new file mode 100644 index 000000000..5a1ce6063 --- /dev/null +++ b/web/src/pages/groups/GroupListPage.ts @@ -0,0 +1,80 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { AKResponse } from "../../api/Client"; +import { TablePage } from "../../elements/table/TablePage"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/SpinnerButton"; +import { TableColumn } from "../../elements/table/Table"; +import { Group } from "../../api/Groups"; + +@customElement("ak-group-list") +export class GroupListPage extends TablePage { + searchEnabled(): boolean { + return true; + } + pageTitle(): string { + return gettext("Groups"); + } + pageDescription(): string { + return gettext("Group users together and give them permissions based on the membership."); + } + pageIcon(): string { + return gettext("pf-icon pf-icon-users"); + } + + @property() + order = "slug"; + + apiEndpoint(page: number): Promise> { + return Group.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn("Name", "name"), + new TableColumn("Parent", "parent"), + new TableColumn("Members"), + new TableColumn("Superuser privileges?"), + new TableColumn(""), + ]; + } + + row(item: Group): TemplateResult[] { + return [ + html`${item.name}`, + html`${item.parent || "-"}`, + html`${item.users.length}`, + html`${item.is_superuser ? "Yes" : "No"}`, + html` + + + ${gettext("Edit")} + +
+
+ + + ${gettext("Delete")} + +
+
`, + ]; + } + + renderToolbar(): TemplateResult { + return html` + + + ${gettext("Create")} + +
+
+ ${super.renderToolbar()} + `; + } +} diff --git a/web/src/routes.ts b/web/src/routes.ts index 63e114b96..0675d2ba2 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -19,6 +19,7 @@ import "./pages/providers/ProviderListPage"; import "./pages/providers/ProviderViewPage"; import "./pages/sources/SourcesListPage"; import "./pages/sources/SourceViewPage"; +import "./pages/groups/GroupListPage"; export const ROUTES: Route[] = [ // Prevent infinite Shell loops @@ -39,6 +40,7 @@ export const ROUTES: Route[] = [ return html``; }), new Route(new RegExp("^/policies$"), html``), + new Route(new RegExp("^/groups$"), html``), new Route(new RegExp("^/flows$"), html``), new Route(new RegExp(`^/flows/(?${SLUG_REGEX})$`)).then((args) => { return html``; From 75d0bd01c26200eb03d351f5c8379b37dd56f720 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 17:19:48 +0100 Subject: [PATCH 21/34] admin: remove StageBinding list --- .../administration/stage_binding/list.html | 125 ------------------ authentik/admin/urls.py | 5 - authentik/admin/views/stages_bindings.py | 23 +--- 3 files changed, 5 insertions(+), 148 deletions(-) delete mode 100644 authentik/admin/templates/administration/stage_binding/list.html diff --git a/authentik/admin/templates/administration/stage_binding/list.html b/authentik/admin/templates/administration/stage_binding/list.html deleted file mode 100644 index c4a772a62..000000000 --- a/authentik/admin/templates/administration/stage_binding/list.html +++ /dev/null @@ -1,125 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load authentik_utils %} - -{% block content %} -
-
-

- - {% trans 'Stage Bindings' %} -

-

{% trans "Bind existing Stages to Flows." %}

-
-
-
-
- {% if object_list %} -
-
-
- - - {% trans 'Create' %} - -
-
- -
- {% include 'partials/pagination.html' %} -
-
- - - - - - - - - - - {% regroup object_list by target as grouped_bindings %} - {% for flow in grouped_bindings %} - - - - - - - {% for binding in flow.list %} - - - - - - - {% endfor %} - {% endfor %} - -
{% trans 'Order' %}{% trans 'Name' %}{% trans 'Stage Type' %}
- {% blocktrans with slug=flow.grouper.slug %} - Flow {{ slug }} - {% endblocktrans %} -
- - {{ binding.order }} - - -
-
{{ binding.target.slug }}
- - {{ binding.target.name }} - -
-
-
-
- {{ binding.stage.name }} -
- - {{ binding.stage }} - -
-
- - - {% trans 'Update' %} - -
-
- - - {% trans 'Delete' %} - -
-
-
-
- {% include 'partials/pagination.html' %} -
- {% else %} -
-
- -

- {% trans 'No Flow-Stage Bindings.' %} -

-
- {% trans 'Currently no flow-stage bindings exist. Click the button below to create one.' %} -
- - - {% trans 'Create' %} - -
-
-
-
- {% endif %} -
-
-{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index d2deda6e9..b68496059 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -145,11 +145,6 @@ urlpatterns = [ name="stage-delete", ), # Stage bindings - path( - "stages/bindings/", - stages_bindings.StageBindingListView.as_view(), - name="stage-bindings", - ), path( "stages/bindings/create/", stages_bindings.StageBindingCreateView.as_view(), diff --git a/authentik/admin/views/stages_bindings.py b/authentik/admin/views/stages_bindings.py index 7e416d19f..ef7e0daef 100644 --- a/authentik/admin/views/stages_bindings.py +++ b/authentik/admin/views/stages_bindings.py @@ -7,32 +7,19 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.db.models import Max -from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from django.views.generic import ListView, UpdateView -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from django.views.generic import UpdateView +from guardian.mixins import PermissionRequiredMixin from authentik.admin.views.utils import ( BackSuccessUrlMixin, DeleteMessageView, - UserPaginateListMixin, ) from authentik.flows.forms import FlowStageBindingForm from authentik.flows.models import Flow, FlowStageBinding from authentik.lib.views import CreateAssignPermView -class StageBindingListView( - LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView -): - """Show list of all flows""" - - model = FlowStageBinding - permission_required = "authentik_flows.view_flowstagebinding" - ordering = ["target", "order"] - template_name = "administration/stage_binding/list.html" - - class StageBindingCreateView( SuccessMessageMixin, BackSuccessUrlMixin, @@ -47,7 +34,7 @@ class StageBindingCreateView( form_class = FlowStageBindingForm template_name = "generic/create.html" - success_url = reverse_lazy("authentik_admin:stage-bindings") + success_url = "/" success_message = _("Successfully created StageBinding") def get_initial(self) -> dict[str, Any]: @@ -79,7 +66,7 @@ class StageBindingUpdateView( form_class = FlowStageBindingForm template_name = "generic/update.html" - success_url = reverse_lazy("authentik_admin:stage-bindings") + success_url = "/" success_message = _("Successfully updated StageBinding") @@ -92,5 +79,5 @@ class StageBindingDeleteView( permission_required = "authentik_flows.delete_flowstagebinding" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:stage-bindings") + success_url = "/" success_message = _("Successfully deleted FlowStageBinding") From 8008918d8b671f81edae1759e4eacd059be87c0e Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 17:21:11 +0100 Subject: [PATCH 22/34] admin: remove PolicyBinding list --- .../administration/policy_binding/list.html | 119 ------------------ authentik/admin/urls.py | 5 - authentik/admin/views/policies_bindings.py | 49 ++------ authentik/admin/views/stages_bindings.py | 5 +- authentik/flows/api.py | 3 +- 5 files changed, 9 insertions(+), 172 deletions(-) delete mode 100644 authentik/admin/templates/administration/policy_binding/list.html diff --git a/authentik/admin/templates/administration/policy_binding/list.html b/authentik/admin/templates/administration/policy_binding/list.html deleted file mode 100644 index ee581d6c8..000000000 --- a/authentik/admin/templates/administration/policy_binding/list.html +++ /dev/null @@ -1,119 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load authentik_utils %} - -{% block content %} -
-
-

- - {% trans 'Policy Bindings' %} -

-

{% trans "Bind existing Policies to Models accepting policies." %}

-
-
-
-
- {% if object_list %} -
-
-
- - - {% trans 'Create' %} - -
-
- -
- {% include 'partials/pagination.html' %} -
-
- - - - - - - - - - - - {% for pbm in object_list %} - - - - - - - - {% for binding in pbm.bindings %} - - - - - - - - {% endfor %} - {% endfor %} - -
{% trans 'Policy' %}{% trans 'Enabled' %}{% trans 'Order' %}{% trans 'Timeout' %}
- {{ pbm }} - - {{ pbm|fieldtype }} - -
-
{{ binding.policy }}
- - {{ binding.policy|fieldtype }} - -
-
{{ binding.enabled }}
-
-
{{ binding.order }}
-
-
{{ binding.timeout }}
-
- - - {% trans 'Edit' %} - -
-
- - - {% trans 'Delete' %} - -
-
-
-
- {% include 'partials/pagination.html' %} -
- {% else %} -
-
- -

- {% trans 'No Policy Bindings.' %} -

-
- {% trans 'Currently no policy bindings exist. Click the button below to create one.' %} -
- - - {% trans 'Create' %} - -
-
-
-
- {% endif %} -
-
-{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index b68496059..1e5191fcd 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -90,11 +90,6 @@ urlpatterns = [ name="policy-test", ), # Policy bindings - path( - "policies/bindings/", - policies_bindings.PolicyBindingListView.as_view(), - name="policies-bindings", - ), path( "policies/bindings/create/", policies_bindings.PolicyBindingCreateView.as_view(), diff --git a/authentik/admin/views/policies_bindings.py b/authentik/admin/views/policies_bindings.py index be19eb815..3dfd8ca9a 100644 --- a/authentik/admin/views/policies_bindings.py +++ b/authentik/admin/views/policies_bindings.py @@ -6,52 +6,17 @@ from django.contrib.auth.mixins import ( PermissionRequiredMixin as DjangoPermissionRequiredMixin, ) from django.contrib.messages.views import SuccessMessageMixin -from django.db.models import Max, QuerySet -from django.urls import reverse_lazy +from django.db.models import Max from django.utils.translation import gettext as _ -from django.views.generic import ListView, UpdateView -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin -from guardian.shortcuts import get_objects_for_user +from django.views.generic import UpdateView +from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import ( - BackSuccessUrlMixin, - DeleteMessageView, - UserPaginateListMixin, -) +from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView from authentik.lib.views import CreateAssignPermView from authentik.policies.forms import PolicyBindingForm from authentik.policies.models import PolicyBinding, PolicyBindingModel -class PolicyBindingListView( - LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView -): - """Show list of all policies""" - - model = PolicyBinding - permission_required = "authentik_policies.view_policybinding" - ordering = ["order", "target"] - template_name = "administration/policy_binding/list.html" - - def get_queryset(self) -> QuerySet: - # Since `select_subclasses` does not work with a foreign key, we have to do two queries here - # First, get all pbm objects that have bindings attached - objects = ( - get_objects_for_user( - self.request.user, "authentik_policies.view_policybindingmodel" - ) - .filter(policies__isnull=False) - .select_subclasses() - .select_related() - .order_by("pk") - ) - for pbm in objects: - pbm.bindings = get_objects_for_user( - self.request.user, self.permission_required - ).filter(target__pk=pbm.pbm_uuid) - return objects - - class PolicyBindingCreateView( SuccessMessageMixin, BackSuccessUrlMixin, @@ -66,7 +31,7 @@ class PolicyBindingCreateView( form_class = PolicyBindingForm template_name = "generic/create.html" - success_url = reverse_lazy("authentik_admin:policies-bindings") + success_url = "/" success_message = _("Successfully created PolicyBinding") def get_initial(self) -> dict[str, Any]: @@ -100,7 +65,7 @@ class PolicyBindingUpdateView( form_class = PolicyBindingForm template_name = "generic/update.html" - success_url = reverse_lazy("authentik_admin:policies-bindings") + success_url = "/" success_message = _("Successfully updated PolicyBinding") @@ -113,5 +78,5 @@ class PolicyBindingDeleteView( permission_required = "authentik_policies.delete_policybinding" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:policies-bindings") + success_url = "/" success_message = _("Successfully deleted PolicyBinding") diff --git a/authentik/admin/views/stages_bindings.py b/authentik/admin/views/stages_bindings.py index ef7e0daef..7aae1d33c 100644 --- a/authentik/admin/views/stages_bindings.py +++ b/authentik/admin/views/stages_bindings.py @@ -11,10 +11,7 @@ from django.utils.translation import gettext as _ from django.views.generic import UpdateView from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import ( - BackSuccessUrlMixin, - DeleteMessageView, -) +from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView from authentik.flows.forms import FlowStageBindingForm from authentik.flows.models import Flow, FlowStageBinding from authentik.lib.views import CreateAssignPermView diff --git a/authentik/flows/api.py b/authentik/flows/api.py index 03b84ad69..4ca0ff982 100644 --- a/authentik/flows/api.py +++ b/authentik/flows/api.py @@ -7,7 +7,6 @@ from django.shortcuts import get_object_or_404, reverse from drf_yasg2.utils import swagger_auto_schema from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action -from rest_framework.mixins import ListModelMixin from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ( @@ -16,7 +15,7 @@ from rest_framework.serializers import ( Serializer, SerializerMethodField, ) -from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet +from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from authentik.core.api.utils import ( CacheSerializer, From 865f652476bc68911e6343345cd0068199012e6c Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 17:49:34 +0100 Subject: [PATCH 23/34] web: migrate Outpost Service Connection to web --- .../outpost_service_connection/list.html | 153 ------------------ authentik/admin/urls.py | 5 - .../views/outposts_service_connections.py | 34 +--- .../api/outpost_service_connections.py | 81 ++++++++-- swagger.yaml | 147 ++++++++++++++--- web/src/api/Outposts.ts | 41 ++++- web/src/interfaces/AdminInterface.ts | 2 +- web/src/pages/flows/BoundStagesList.ts | 1 + .../OutpostServiceConnectionListPage.ts | 102 ++++++++++++ web/src/pages/policies/PolicyListPage.ts | 4 +- web/src/routes.ts | 2 + 11 files changed, 353 insertions(+), 219 deletions(-) delete mode 100644 authentik/admin/templates/administration/outpost_service_connection/list.html create mode 100644 web/src/pages/outposts/OutpostServiceConnectionListPage.ts diff --git a/authentik/admin/templates/administration/outpost_service_connection/list.html b/authentik/admin/templates/administration/outpost_service_connection/list.html deleted file mode 100644 index b43dcf2dc..000000000 --- a/authentik/admin/templates/administration/outpost_service_connection/list.html +++ /dev/null @@ -1,153 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load humanize %} -{% load authentik_utils %} - -{% block content %} -
-
-

- - {% trans 'Outpost Service-Connections' %} -

-

{% trans "Outpost Service-Connections define how authentik connects to external platforms to manage and deploy Outposts." %}

-
-
-
-
- {% if object_list %} -
-
- {% include 'partials/toolbar_search.html' %} -
- - - - - -
- {% include 'partials/pagination.html' %} -
-
- - - - - - - - - - - - {% for sc in object_list %} - - - - - - - - {% endfor %} - -
{% trans 'Name' %}{% trans 'Type' %}{% trans 'Local?' %}{% trans 'Status' %}
- {{ sc.name }} - - - {{ sc|verbose_name }} - - - - {{ sc.local|yesno:"Yes,No" }} - - - - {% if sc.state.healthy %} - {{ sc.state.version }} - {% else %} - {% trans 'Unhealthy' %} - {% endif %} - - - - - {% trans 'Edit' %} - -
-
- - - {% trans 'Delete' %} - -
-
-
-
- {% include 'partials/pagination.html' %} -
- {% else %} -
-
- {% include 'partials/toolbar_search.html' %} -
-
-
-
- -

- {% trans 'No Outpost Service Connections.' %} -

-
- {% if request.GET.search != "" %} - {% trans "Your search query doesn't match any outposts." %} - {% else %} - {% trans 'Currently no service connections exist. Click the button below to create one.' %} - {% endif %} -
- - - - -
-
- {% endif %} -
-
-{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index 1e5191fcd..7432e7c29 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -308,11 +308,6 @@ urlpatterns = [ name="outpost-delete", ), # Outpost Service Connections - path( - "outpost_service_connections/", - outposts_service_connections.OutpostServiceConnectionListView.as_view(), - name="outpost-service-connections", - ), path( "outpost_service_connections/create/", outposts_service_connections.OutpostServiceConnectionCreateView.as_view(), diff --git a/authentik/admin/views/outposts_service_connections.py b/authentik/admin/views/outposts_service_connections.py index a1aded022..4f2b107ed 100644 --- a/authentik/admin/views/outposts_service_connections.py +++ b/authentik/admin/views/outposts_service_connections.py @@ -4,38 +4,18 @@ from django.contrib.auth.mixins import ( PermissionRequiredMixin as DjangoPermissionRequiredMixin, ) from django.contrib.messages.views import SuccessMessageMixin -from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from guardian.mixins import PermissionRequiredMixin from authentik.admin.views.utils import ( BackSuccessUrlMixin, DeleteMessageView, InheritanceCreateView, - InheritanceListView, InheritanceUpdateView, - SearchListMixin, - UserPaginateListMixin, ) from authentik.outposts.models import OutpostServiceConnection -class OutpostServiceConnectionListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - InheritanceListView, -): - """Show list of all outpost-service-connections""" - - model = OutpostServiceConnection - permission_required = "authentik_outposts.add_outpostserviceconnection" - template_name = "administration/outpost_service_connection/list.html" - ordering = "pk" - search_fields = ["pk", "name"] - - class OutpostServiceConnectionCreateView( SuccessMessageMixin, BackSuccessUrlMixin, @@ -49,8 +29,8 @@ class OutpostServiceConnectionCreateView( permission_required = "authentik_outposts.add_outpostserviceconnection" template_name = "generic/create.html" - success_url = reverse_lazy("authentik_admin:outpost-service-connections") - success_message = _("Successfully created OutpostServiceConnection") + success_url = "/" + success_message = _("Successfully created Outpost Service Connection") class OutpostServiceConnectionUpdateView( @@ -66,8 +46,8 @@ class OutpostServiceConnectionUpdateView( permission_required = "authentik_outposts.change_outpostserviceconnection" template_name = "generic/update.html" - success_url = reverse_lazy("authentik_admin:outpost-service-connections") - success_message = _("Successfully updated OutpostServiceConnection") + success_url = "/" + success_message = _("Successfully updated Outpost Service Connection") class OutpostServiceConnectionDeleteView( @@ -79,5 +59,5 @@ class OutpostServiceConnectionDeleteView( permission_required = "authentik_outposts.delete_outpostserviceconnection" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:outpost-service-connections") - success_message = _("Successfully deleted OutpostServiceConnection") + success_url = "/" + success_message = _("Successfully deleted Outpost Service Connection") diff --git a/authentik/outposts/api/outpost_service_connections.py b/authentik/outposts/api/outpost_service_connections.py index 46f142ae6..986002b4d 100644 --- a/authentik/outposts/api/outpost_service_connections.py +++ b/authentik/outposts/api/outpost_service_connections.py @@ -1,7 +1,19 @@ """Outpost API Views""" -from rest_framework.serializers import ModelSerializer +from dataclasses import asdict + +from django.db.models.base import Model +from django.shortcuts import reverse +from drf_yasg2.utils import swagger_auto_schema +from rest_framework.decorators import action +from rest_framework.fields import BooleanField, CharField, SerializerMethodField +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.viewsets import ModelViewSet +from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer +from authentik.lib.templatetags.authentik_utils import verbose_name +from authentik.lib.utils.reflection import all_subclasses from authentik.outposts.models import ( DockerServiceConnection, KubernetesServiceConnection, @@ -9,32 +21,79 @@ from authentik.outposts.models import ( ) -class ServiceConnectionSerializer(ModelSerializer): +class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer): """ServiceConnection Serializer""" + object_type = SerializerMethodField() + + def get_object_type(self, obj: OutpostServiceConnection) -> str: + """Get object type so that we know which API Endpoint to use to get the full object""" + return obj._meta.object_name.lower().replace("serviceconnection", "") + class Meta: model = OutpostServiceConnection - fields = ["pk", "name"] + fields = [ + "pk", + "name", + "local", + "object_type", + "verbose_name", + "verbose_name_plural", + ] + + +class ServiceConnectionStateSerializer(Serializer): + """Serializer for Service connection state""" + + healthy = BooleanField(read_only=True) + version = CharField(read_only=True) + + def create(self, validated_data: dict) -> Model: + raise NotImplementedError + + def update(self, instance: Model, validated_data: dict) -> Model: + raise NotImplementedError class ServiceConnectionViewSet(ModelViewSet): """ServiceConnection Viewset""" - queryset = OutpostServiceConnection.objects.all() + queryset = OutpostServiceConnection.objects.select_subclasses() serializer_class = ServiceConnectionSerializer + @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)}) + @action(detail=False) + def types(self, request: Request) -> Response: + """Get all creatable service connection types""" + data = [] + for subclass in all_subclasses(self.queryset.model): + data.append( + { + "name": verbose_name(subclass), + "description": subclass.__doc__, + "link": reverse("authentik_admin:outpost-service-connection-create") + + f"?type={subclass.__name__}", + } + ) + return Response(TypeCreateSerializer(data, many=True).data) -class DockerServiceConnectionSerializer(ModelSerializer): + @swagger_auto_schema(responses={200: ServiceConnectionStateSerializer(many=False)}) + @action(detail=True) + # pylint: disable=unused-argument, invalid-name + def state(self, request: Request, pk: str) -> Response: + """Get the service connection's state""" + connection = self.get_object() + return Response(asdict(connection.state)) + + +class DockerServiceConnectionSerializer(ServiceConnectionSerializer): """DockerServiceConnection Serializer""" class Meta: model = DockerServiceConnection - fields = [ - "pk", - "name", - "local", + fields = ServiceConnectionSerializer.Meta.fields + [ "url", "tls_verification", "tls_authentication", @@ -48,13 +107,13 @@ class DockerServiceConnectionViewSet(ModelViewSet): serializer_class = DockerServiceConnectionSerializer -class KubernetesServiceConnectionSerializer(ModelSerializer): +class KubernetesServiceConnectionSerializer(ServiceConnectionSerializer): """KubernetesServiceConnection Serializer""" class Meta: model = KubernetesServiceConnection - fields = ["pk", "name", "local", "kubeconfig"] + fields = ServiceConnectionSerializer.Meta.fields + ["kubeconfig"] class KubernetesServiceConnectionViewSet(ModelViewSet): diff --git a/swagger.yaml b/swagger.yaml index 3274888bc..ad050bf70 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -2169,6 +2169,42 @@ paths: tags: - outposts parameters: [] + /outposts/service_connections/all/types/: + get: + operationId: outposts_service_connections_all_types + description: Get all creatable service connection types + parameters: + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: Types of an object that can be created + schema: + description: '' + type: array + items: + $ref: '#/definitions/TypeCreate' + tags: + - outposts + parameters: [] /outposts/service_connections/all/{uuid}/: get: operationId: outposts_service_connections_all_read @@ -2229,6 +2265,25 @@ paths: required: true type: string format: uuid + /outposts/service_connections/all/{uuid}/state/: + get: + operationId: outposts_service_connections_all_state + description: Get the service connection's state + parameters: [] + responses: + '200': + description: Serializer for Service connection state + schema: + $ref: '#/definitions/ServiceConnectionState' + tags: + - outposts + parameters: + - name: uuid + in: path + description: A UUID string identifying this Outpost Service-Connection. + required: true + type: string + format: uuid /outposts/service_connections/docker/: get: operationId: outposts_service_connections_docker_list @@ -8797,6 +8852,55 @@ definitions: title: Name type: string minLength: 1 + local: + title: Local + description: If enabled, use the local connection. Required Docker socket/Kubernetes + Integration + type: boolean + object_type: + title: Object type + type: string + readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true + TypeCreate: + description: Types of an object that can be created + type: object + properties: + name: + title: Name + type: string + readOnly: true + minLength: 1 + description: + title: Description + type: string + readOnly: true + minLength: 1 + link: + title: Link + type: string + readOnly: true + minLength: 1 + ServiceConnectionState: + description: Serializer for Service connection state + type: object + properties: + healthy: + title: Healthy + type: boolean + readOnly: true + version: + title: Version + type: string + readOnly: true + minLength: 1 DockerServiceConnection: description: DockerServiceConnection Serializer required: @@ -8818,6 +8922,18 @@ definitions: description: If enabled, use the local connection. Required Docker socket/Kubernetes Integration type: boolean + object_type: + title: Object type + type: string + readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true url: title: Url description: Can be in the format of 'unix://' when connecting to a @@ -8859,6 +8975,18 @@ definitions: description: If enabled, use the local connection. Required Docker socket/Kubernetes Integration type: boolean + object_type: + title: Object type + type: string + readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true kubeconfig: title: Kubeconfig description: Paste your kubeconfig here. authentik will automatically use @@ -8898,25 +9026,6 @@ definitions: title: Bound to type: integer readOnly: true - TypeCreate: - description: Types of an object that can be created - type: object - properties: - name: - title: Name - type: string - readOnly: true - minLength: 1 - description: - title: Description - type: string - readOnly: true - minLength: 1 - link: - title: Link - type: string - readOnly: true - minLength: 1 PolicyBinding: description: PolicyBinding Serializer required: diff --git a/web/src/api/Outposts.ts b/web/src/api/Outposts.ts index 6a84cb833..10c60ed7e 100644 --- a/web/src/api/Outposts.ts +++ b/web/src/api/Outposts.ts @@ -1,5 +1,5 @@ import { DefaultClient, AKResponse, QueryArguments } from "./Client"; -import { Provider } from "./Providers"; +import { Provider, TypeCreate } from "./Providers"; export interface OutpostHealth { last_seen: number; @@ -38,3 +38,42 @@ export class Outpost { return `/administration/outposts/${rest}`; } } + +export interface OutpostServiceConnectionState { + version: string; + healthy: boolean; +} + +export class OutpostServiceConnection { + pk: string; + name: string; + local: boolean; + object_type: string; + verbose_name: string; + verbose_name_plural: string; + + constructor() { + throw Error(); + } + + static get(pk: string): Promise { + return DefaultClient.fetch(["outposts", "service_connections", "all", pk]); + } + + static list(filter?: QueryArguments): Promise> { + return DefaultClient.fetch>(["outposts", "service_connections", "all"], filter); + } + + static state(pk: string): Promise { + return DefaultClient.fetch(["outposts", "service_connections", "all", pk, "state"]); + } + + static getTypes(): Promise { + return DefaultClient.fetch(["outposts", "service_connections", "all", "types"]); + } + + static adminUrl(rest: string): string { + return `/administration/outpost_service_connections/${rest}`; + } + +} diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index 75d791835..d728a11d6 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -28,7 +28,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ ), new SidebarItem("Providers", "/providers"), new SidebarItem("Outposts", "/outposts"), - new SidebarItem("Outpost Service Connections", "/administration/outpost_service_connections/"), + new SidebarItem("Outpost Service Connections", "/outpost-service-connections"), ).when((): Promise => { return User.me().then(u => u.is_superuser); }), diff --git a/web/src/pages/flows/BoundStagesList.ts b/web/src/pages/flows/BoundStagesList.ts index 464ef47a4..a1b95c3db 100644 --- a/web/src/pages/flows/BoundStagesList.ts +++ b/web/src/pages/flows/BoundStagesList.ts @@ -7,6 +7,7 @@ import "../../elements/Tabs"; import "../../elements/AdminLoginsChart"; import "../../elements/buttons/ModalButton"; import "../../elements/buttons/SpinnerButton"; +import "../../elements/buttons/Dropdown"; import "../../elements/policies/BoundPoliciesList"; import { FlowStageBinding, Stage } from "../../api/Flows"; import { until } from "lit-html/directives/until"; diff --git a/web/src/pages/outposts/OutpostServiceConnectionListPage.ts b/web/src/pages/outposts/OutpostServiceConnectionListPage.ts new file mode 100644 index 000000000..7e8eb4333 --- /dev/null +++ b/web/src/pages/outposts/OutpostServiceConnectionListPage.ts @@ -0,0 +1,102 @@ +import { gettext } from "django"; +import { customElement, property } from "lit-element"; +import { html, TemplateResult } from "lit-html"; +import { AKResponse } from "../../api/Client"; +import { OutpostServiceConnection } from "../../api/Outposts"; +import { TableColumn } from "../../elements/table/Table"; +import { TablePage } from "../../elements/table/TablePage"; + +import "./OutpostHealth"; +import "../../elements/buttons/SpinnerButton"; +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/Dropdown"; +import { until } from "lit-html/directives/until"; + +@customElement("ak-outpost-service-connection-list") +export class OutpostServiceConnectionListPage extends TablePage { + pageTitle(): string { + return "Outpost Service-Connections"; + } + pageDescription(): string | undefined { + return "Outpost Service-Connections define how authentik connects to external platforms to manage and deploy Outposts."; + } + pageIcon(): string { + return "pf-icon pf-icon-integration"; + } + searchEnabled(): boolean { + return true; + } + + apiEndpoint(page: number): Promise> { + return OutpostServiceConnection.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + columns(): TableColumn[] { + return [ + new TableColumn("Name", "name"), + new TableColumn("Type"), + new TableColumn("Local", "local"), + new TableColumn("State"), + new TableColumn(""), + ]; + } + + @property() + order = "name"; + + row(item: OutpostServiceConnection): TemplateResult[] { + return [ + html`${item.name}`, + html`${item.verbose_name}`, + html`${item.local ? "Yes" : "No"}`, + html`${until(OutpostServiceConnection.state(item.pk).then((state) => { + if (state.healthy) { + return html` ${state.version}`; + } + return html` ${gettext("Unhealthy")}`; + }), html``)}`, + html` + + + ${gettext("Edit")} + +
+
+ + + ${gettext("Delete")} + +
+
`, + ]; + } + + renderToolbar(): TemplateResult { + return html` + + + + + ${super.renderToolbar()}`; + } + +} diff --git a/web/src/pages/policies/PolicyListPage.ts b/web/src/pages/policies/PolicyListPage.ts index 503e2c660..90ad15e08 100644 --- a/web/src/pages/policies/PolicyListPage.ts +++ b/web/src/pages/policies/PolicyListPage.ts @@ -31,7 +31,7 @@ export class PolicyListPage extends TablePage { apiEndpoint(page: number): Promise> { return Policy.list({ ordering: this.order, - page: page, + page: page, search: this.search || "", }); } @@ -54,7 +54,7 @@ export class PolicyListPage extends TablePage { ${gettext(`Assigned to ${item.bound_to} objects.`)}
`: html` - ${gettext("Warning: Policy is not assigned.")}/small>`} + ${gettext("Warning: Policy is not assigned.")}`} `, html`${item.verbose_name}`, html` diff --git a/web/src/routes.ts b/web/src/routes.ts index 0675d2ba2..453325ad8 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -13,6 +13,7 @@ import "./pages/flows/FlowListPage"; import "./pages/flows/FlowViewPage"; import "./pages/LibraryPage"; import "./pages/outposts/OutpostListPage"; +import "./pages/outposts/OutpostServiceConnectionListPage"; import "./pages/policies/PolicyListPage"; import "./pages/property-mappings/PropertyMappingListPage"; import "./pages/providers/ProviderListPage"; @@ -53,5 +54,6 @@ export const ROUTES: Route[] = [ new Route(new RegExp("^/events/rules$"), html``), new Route(new RegExp("^/property-mappings$"), html``), new Route(new RegExp("^/outposts$"), html``), + new Route(new RegExp("^/outpost-service-connections$"), html``), new Route(new RegExp("^/crypto/certificates$"), html``), ]; From d219f65e7a2443cf59b994654c99c725cffb5caf Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 18:12:23 +0100 Subject: [PATCH 24/34] web: migrate System Task list to web --- .../templates/administration/task/list.html | 84 ------------------ authentik/admin/urls.py | 7 -- authentik/admin/views/tasks.py | 23 ----- web/src/api/SystemTask.ts | 33 +++++++ web/src/interfaces/AdminInterface.ts | 2 +- .../pages/system-tasks/SystemTaskListPage.ts | 87 +++++++++++++++++++ web/src/routes.ts | 2 + 7 files changed, 123 insertions(+), 115 deletions(-) delete mode 100644 authentik/admin/templates/administration/task/list.html delete mode 100644 authentik/admin/views/tasks.py create mode 100644 web/src/api/SystemTask.ts create mode 100644 web/src/pages/system-tasks/SystemTaskListPage.ts diff --git a/authentik/admin/templates/administration/task/list.html b/authentik/admin/templates/administration/task/list.html deleted file mode 100644 index 373bff1fc..000000000 --- a/authentik/admin/templates/administration/task/list.html +++ /dev/null @@ -1,84 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load humanize %} -{% load authentik_utils %} - -{% block content %} -
-
-

- - {% trans 'System Tasks' %} -

-

{% trans "Long-running operations which authentik executes in the background." %}

-
-
-
-
-
-
- -
-
- - - - - - - - - - - - - {% for task in object_list %} - - - - - - - - - {% endfor %} - -
{% trans 'Identifier' %}{% trans 'Description' %}{% trans 'Last Run' %}{% trans 'Status' %}{% trans 'Messages' %}
- {{ task.html_name|join:"_­" }} - - - {{ task.task_description }} - - - - {{ task.finish_timestamp|naturaltime }} - - - - {% if task.result.status == task_successful %} - {% trans 'Successful' %} - {% elif task.result.status == task_warning %} - {% trans 'Warning' %} - {% elif task.result.status == task_error %} - {% trans 'Error' %} - {% else %} - {% trans 'Unknown' %} - {% endif %} - - - {% for message in task.result.messages %} -
- {{ message }} -
- {% endfor %} -
- - {% trans 'Retry Task' %} - -
-
-
-{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index 7432e7c29..559550204 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -20,7 +20,6 @@ from authentik.admin.views import ( stages_bindings, stages_invitations, stages_prompts, - tasks, tokens, users, ) @@ -323,12 +322,6 @@ urlpatterns = [ outposts_service_connections.OutpostServiceConnectionDeleteView.as_view(), name="outpost-service-connection-delete", ), - # Tasks - path( - "tasks/", - tasks.TaskListView.as_view(), - name="tasks", - ), # Event Notification Transpots path( "events/transports/create/", diff --git a/authentik/admin/views/tasks.py b/authentik/admin/views/tasks.py deleted file mode 100644 index fdc69d5f4..000000000 --- a/authentik/admin/views/tasks.py +++ /dev/null @@ -1,23 +0,0 @@ -"""authentik Tasks List""" -from typing import Any - -from django.views.generic.base import TemplateView - -from authentik.admin.mixins import AdminRequiredMixin -from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus - - -class TaskListView(AdminRequiredMixin, TemplateView): - """Show list of all background tasks""" - - template_name = "administration/task/list.html" - - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - kwargs = super().get_context_data(**kwargs) - kwargs["object_list"] = sorted( - TaskInfo.all().values(), key=lambda x: x.task_name - ) - kwargs["task_successful"] = TaskResultStatus.SUCCESSFUL - kwargs["task_warning"] = TaskResultStatus.WARNING - kwargs["task_error"] = TaskResultStatus.ERROR - return kwargs diff --git a/web/src/api/SystemTask.ts b/web/src/api/SystemTask.ts new file mode 100644 index 000000000..222873286 --- /dev/null +++ b/web/src/api/SystemTask.ts @@ -0,0 +1,33 @@ +import { DefaultClient, QueryArguments } from "./Client"; + +export enum TaskStatus { + SUCCESSFUL = 1, + WARNING = 2, + ERROR = 4, +} + +export class SystemTask { + + task_name: string; + task_description: string; + task_finish_timestamp: number; + status: TaskStatus; + messages: string[]; + + constructor() { + throw Error(); + } + + static get(task_name: string): Promise { + return DefaultClient.fetch(["admin", "system_tasks", task_name]); + } + + static list(filter?: QueryArguments): Promise { + return DefaultClient.fetch(["admin", "system_tasks"], filter); + } + + static retry(task_name: string): string { + return DefaultClient.makeUrl(["admin", "system_tasks", task_name, "retry"]); + } + +} diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index d728a11d6..d56670720 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -8,7 +8,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ new SidebarItem("Library", "/library"), new SidebarItem("Monitor").children( new SidebarItem("Overview", "/administration/overview"), - new SidebarItem("System Tasks", "/administration/tasks/"), + new SidebarItem("System Tasks", "/administration/system-tasks"), ).when((): Promise => { return User.me().then(u => u.is_superuser); }), diff --git a/web/src/pages/system-tasks/SystemTaskListPage.ts b/web/src/pages/system-tasks/SystemTaskListPage.ts new file mode 100644 index 000000000..bce1e09d3 --- /dev/null +++ b/web/src/pages/system-tasks/SystemTaskListPage.ts @@ -0,0 +1,87 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { AKResponse } from "../../api/Client"; +import { TablePage } from "../../elements/table/TablePage"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/SpinnerButton"; +import "../../elements/buttons/ActionButton"; +import { TableColumn } from "../../elements/table/Table"; +import { SystemTask, TaskStatus } from "../../api/SystemTask"; + +@customElement("ak-system-task-list") +export class SystemTaskListPage extends TablePage { + searchEnabled(): boolean { + return false; + } + pageTitle(): string { + return gettext("System Tasks"); + } + pageDescription(): string { + return gettext("Long-running operations which authentik executes in the background."); + } + pageIcon(): string { + return gettext("pf-icon pf-icon-automation"); + } + + @property() + order = "slug"; + + apiEndpoint(page: number): Promise> { + return SystemTask.list({ + ordering: this.order, + page: page, + }).then((tasks) => { + return { + pagination: { + count: tasks.length, + total_pages: 1, + start_index: 0, + end_index: tasks.length, + current: 1, + }, + results: tasks, + }; + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn("Identifier", "task_name"), + new TableColumn("Description"), + new TableColumn("Last run"), + new TableColumn("Status"), + new TableColumn("Messages"), + new TableColumn(""), + ]; + } + + taskStatus(task: SystemTask): TemplateResult { + switch (task.status) { + case TaskStatus.SUCCESSFUL: + return html` ${gettext("Successful")}`; + case TaskStatus.WARNING: + return html` ${gettext("Warning")}`; + case TaskStatus.ERROR: + return html` ${gettext("Error")}`; + default: + return html` ${gettext("Unknown")}`; + } + } + + row(item: SystemTask): TemplateResult[] { + return [ + html`${item.task_name}`, + html`${item.task_description}`, + html`${new Date(item.task_finish_timestamp * 1000).toLocaleString()}`, + this.taskStatus(item), + html`${item.messages.map(m => { + return html`
  • ${m}
  • `; + })}`, + html` + ${gettext("Retry Task")} + `, + ]; + } + +} diff --git a/web/src/routes.ts b/web/src/routes.ts index 453325ad8..0bcf43ced 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -21,6 +21,7 @@ import "./pages/providers/ProviderViewPage"; import "./pages/sources/SourcesListPage"; import "./pages/sources/SourceViewPage"; import "./pages/groups/GroupListPage"; +import "./pages/system-tasks/SystemTaskListPage"; export const ROUTES: Route[] = [ // Prevent infinite Shell loops @@ -28,6 +29,7 @@ export const ROUTES: Route[] = [ new Route(new RegExp("^#.*")).redirect("/library"), new Route(new RegExp("^/library$"), html``), new Route(new RegExp("^/administration/overview$"), html``), + new Route(new RegExp("^/administration/system-tasks$"), html``), new Route(new RegExp("^/providers$"), html``), new Route(new RegExp(`^/providers/(?${ID_REGEX})$`)).then((args) => { return html``; From fd28f37c0d7df307e5b250b299558c476c047fb9 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 18:43:57 +0100 Subject: [PATCH 25/34] web: migrate User list to web --- .../templates/administration/user/list.html | 125 ------------------ authentik/admin/urls.py | 1 - authentik/admin/views/users.py | 38 +----- authentik/core/api/users.py | 11 +- swagger.yaml | 10 ++ web/src/api/Users.ts | 16 ++- web/src/elements/buttons/SpinnerButton.ts | 24 ++-- web/src/interfaces/AdminInterface.ts | 2 +- web/src/pages/users/UserListPage.ts | 113 ++++++++++++++++ web/src/routes.ts | 2 + 10 files changed, 170 insertions(+), 172 deletions(-) delete mode 100644 authentik/admin/templates/administration/user/list.html create mode 100644 web/src/pages/users/UserListPage.ts diff --git a/authentik/admin/templates/administration/user/list.html b/authentik/admin/templates/administration/user/list.html deleted file mode 100644 index fe291cb51..000000000 --- a/authentik/admin/templates/administration/user/list.html +++ /dev/null @@ -1,125 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load authentik_utils %} - -{% block content %} -
    -
    -

    - - {% trans 'Users' %} -

    -
    -
    -
    -
    - {% if object_list %} -
    -
    - {% include 'partials/toolbar_search.html' %} -
    - - - {% trans 'Create' %} - -
    -
    - -
    - {% include 'partials/pagination.html' %} -
    -
    - - - - - - - - - - - {% for user in object_list %} - - - - - - - {% endfor %} - -
    {% trans 'Name' %}{% trans 'Active' %}{% trans 'Last Login' %}
    -
    -
    {{ user.username }}
    - {{ user.name }} -
    -
    - - {{ user.is_active }} - - - - {{ user.last_login }} - - - - - {% trans 'Edit' %} - -
    -
    - {% if user.is_active %} - - - {% trans 'Disable' %} - -
    -
    - {% else %} - - - {% trans 'Enable' %} - -
    -
    - {% endif %} - {% trans 'Reset Password' %} - {% trans 'Impersonate' %} -
    -
    - {% include 'partials/pagination.html' %} -
    - {% else %} -
    -
    - {% include 'partials/toolbar_search.html' %} -
    -
    -
    -
    - -

    - {% trans 'No Users.' %} -

    -
    - {% if request.GET.search != "" %} - {% trans "Your search query doesn't match any users." %} - {% else %} - {% trans 'Currently no users exist. How did you even get here.' %} - {% endif %} -
    - - - {% trans 'Create' %} - -
    -
    -
    -
    - {% endif %} -
    -
    -{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index 559550204..dc6516f18 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -244,7 +244,6 @@ urlpatterns = [ name="property-mapping-test", ), # Users - path("users/", users.UserListView.as_view(), name="users"), path("users/create/", users.UserCreateView.as_view(), name="user-create"), path("users//update/", users.UserUpdateView.as_view(), name="user-update"), path("users//delete/", users.UserDeleteView.as_view(), name="user-delete"), diff --git a/authentik/admin/views/users.py b/authentik/admin/views/users.py index 434a8c0c2..bc6d673cc 100644 --- a/authentik/admin/views/users.py +++ b/authentik/admin/views/users.py @@ -8,46 +8,22 @@ from django.contrib.messages.views import SuccessMessageMixin from django.http import HttpRequest, HttpResponse from django.http.response import HttpResponseRedirect from django.shortcuts import redirect -from django.urls import reverse, reverse_lazy from django.utils.http import urlencode from django.utils.translation import gettext as _ -from django.views.generic import DetailView, ListView, UpdateView +from django.views.generic import DetailView, UpdateView from guardian.mixins import ( - PermissionListMixin, PermissionRequiredMixin, - get_anonymous_user, ) from authentik.admin.forms.users import UserForm from authentik.admin.views.utils import ( BackSuccessUrlMixin, DeleteMessageView, - SearchListMixin, - UserPaginateListMixin, ) from authentik.core.models import Token, User from authentik.lib.views import CreateAssignPermView -class UserListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - ListView, -): - """Show list of all users""" - - model = User - permission_required = "authentik_core.view_user" - ordering = "username" - template_name = "administration/user/list.html" - search_fields = ["username", "name", "attributes"] - - def get_queryset(self): - return super().get_queryset().exclude(pk=get_anonymous_user().pk) - - class UserCreateView( SuccessMessageMixin, BackSuccessUrlMixin, @@ -62,7 +38,7 @@ class UserCreateView( permission_required = "authentik_core.add_user" template_name = "generic/create.html" - success_url = reverse_lazy("authentik_admin:users") + success_url = "/" success_message = _("Successfully created User") @@ -82,7 +58,7 @@ class UserUpdateView( # By default the object's name is user which is used by other checks context_object_name = "object" template_name = "generic/update.html" - success_url = reverse_lazy("authentik_admin:users") + success_url = "/" success_message = _("Successfully updated User") @@ -95,7 +71,7 @@ class UserDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageV # By default the object's name is user which is used by other checks context_object_name = "object" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:users") + success_url = "/" success_message = _("Successfully deleted User") @@ -112,7 +88,7 @@ class UserDisableView( # By default the object's name is user which is used by other checks context_object_name = "object" template_name = "administration/user/disable.html" - success_url = reverse_lazy("authentik_admin:users") + success_url = "/" success_message = _("Successfully disabled User") def delete(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: @@ -135,7 +111,7 @@ class UserEnableView( # By default the object's name is user which is used by other checks context_object_name = "object" - success_url = reverse_lazy("authentik_admin:users") + success_url = "/" success_message = _("Successfully enabled User") def get(self, request: HttpRequest, *args, **kwargs): @@ -165,4 +141,4 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV messages.success( request, _("Password reset link:
    %(link)s
    " % {"link": link}) ) - return redirect("authentik_admin:users") + return redirect("/") diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index e75d451da..1159b3a7d 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -28,7 +28,16 @@ class UserSerializer(ModelSerializer): class Meta: model = User - fields = ["pk", "username", "name", "is_superuser", "email", "avatar"] + fields = [ + "pk", + "username", + "name", + "is_active", + "last_login", + "is_superuser", + "email", + "avatar", + ] class UserViewSet(ModelViewSet): diff --git a/swagger.yaml b/swagger.yaml index ad050bf70..d4d762204 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -8156,6 +8156,16 @@ definitions: description: User's display name. type: string minLength: 1 + is_active: + title: Active + description: Designates whether this user should be treated as active. Unselect + this instead of deleting accounts. + type: boolean + last_login: + title: Last login + type: string + format: date-time + x-nullable: true is_superuser: title: Is superuser type: boolean diff --git a/web/src/api/Users.ts b/web/src/api/Users.ts index 196702bf2..6bf1a7604 100644 --- a/web/src/api/Users.ts +++ b/web/src/api/Users.ts @@ -1,4 +1,4 @@ -import { DefaultClient, AKResponse } from "./Client"; +import { DefaultClient, AKResponse, QueryArguments } from "./Client"; let _globalMePromise: Promise; @@ -9,11 +9,25 @@ export class User { is_superuser: boolean; email: boolean; avatar: string; + is_active: boolean; + last_login: number; constructor() { throw Error(); } + static get(pk: string): Promise { + return DefaultClient.fetch(["core", "users", pk]); + } + + static list(filter?: QueryArguments): Promise> { + return DefaultClient.fetch>(["core", "users"], filter); + } + + static adminUrl(rest: string): string { + return `/administration/users/${rest}`; + } + static me(): Promise { if (!_globalMePromise) { _globalMePromise = DefaultClient.fetch(["core", "users", "me"]); diff --git a/web/src/elements/buttons/SpinnerButton.ts b/web/src/elements/buttons/SpinnerButton.ts index 99aca32fa..eb80c083a 100644 --- a/web/src/elements/buttons/SpinnerButton.ts +++ b/web/src/elements/buttons/SpinnerButton.ts @@ -70,18 +70,18 @@ export class SpinnerButton extends LitElement { @click=${() => this.callAction()} > ${this.isRunning - ? html` - - - - - - ` - : ""} + ? html` + + + + + + ` + : ""} `; } diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index d56670720..8068e7d49 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -47,7 +47,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ return User.me().then(u => u.is_superuser); }), new SidebarItem("Identity & Cryptography").children( - new SidebarItem("User", "/administration/users/"), + new SidebarItem("User", "/users"), new SidebarItem("Groups", "/groups"), new SidebarItem("Certificates", "/crypto/certificates"), new SidebarItem("Tokens", "/administration/tokens/"), diff --git a/web/src/pages/users/UserListPage.ts b/web/src/pages/users/UserListPage.ts new file mode 100644 index 000000000..340a32600 --- /dev/null +++ b/web/src/pages/users/UserListPage.ts @@ -0,0 +1,113 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { AKResponse } from "../../api/Client"; +import { TablePage } from "../../elements/table/TablePage"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/Dropdown"; +import { TableColumn } from "../../elements/table/Table"; +import { User } from "../../api/Users"; + +@customElement("ak-user-list") +export class UserListPage extends TablePage { + searchEnabled(): boolean { + return true; + } + pageTitle(): string { + return gettext("Users"); + } + pageDescription(): string { + return ""; + } + pageIcon(): string { + return gettext("pf-icon pf-icon-user"); + } + + @property() + order = "username"; + + apiEndpoint(page: number): Promise> { + return User.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn("Name", "username"), + new TableColumn("Active", "active"), + new TableColumn("Last login", "last_login"), + new TableColumn(""), + ]; + } + + row(item: User): TemplateResult[] { + return [ + html`
    +
    ${item.username}
    + ${item.name} +
    `, + html`${item.is_active ? "Yes" : "No"}`, + html`${new Date(item.last_login * 1000).toLocaleString()}`, + html` + + + ${gettext("Edit")} + +
    +
    + + + + +
    + ${gettext("Reset Password")} + + + ${gettext("Impersonate")} + `, + ]; + } + + renderToolbar(): TemplateResult { + return html` + + + ${gettext("Create")} + +
    +
    + ${super.renderToolbar()} + `; + } +} diff --git a/web/src/routes.ts b/web/src/routes.ts index 0bcf43ced..fe7fd922b 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -21,6 +21,7 @@ import "./pages/providers/ProviderViewPage"; import "./pages/sources/SourcesListPage"; import "./pages/sources/SourceViewPage"; import "./pages/groups/GroupListPage"; +import "./pages/users/UserListPage"; import "./pages/system-tasks/SystemTaskListPage"; export const ROUTES: Route[] = [ @@ -44,6 +45,7 @@ export const ROUTES: Route[] = [ }), new Route(new RegExp("^/policies$"), html``), new Route(new RegExp("^/groups$"), html``), + new Route(new RegExp("^/users$"), html``), new Route(new RegExp("^/flows$"), html``), new Route(new RegExp(`^/flows/(?${SLUG_REGEX})$`)).then((args) => { return html``; From 6597d5bd28c3175c8c2e297bdac3eee74b211698 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 18:59:24 +0100 Subject: [PATCH 26/34] web: migrate Token List to web --- .../templates/administration/token/list.html | 102 ----------- authentik/admin/urls.py | 1 - authentik/admin/views/tokens.py | 33 +--- authentik/admin/views/users.py | 11 +- authentik/core/api/groups.py | 2 + authentik/core/api/tokens.py | 27 ++- authentik/core/api/users.py | 3 + authentik/flows/api.py | 2 + .../api/outpost_service_connections.py | 2 + authentik/policies/api.py | 1 + swagger.yaml | 172 +++++++++++++----- web/src/api/Tokens.ts | 46 ++++- web/src/elements/buttons/TokenCopyButton.ts | 4 +- web/src/elements/table/TablePagination.ts | 2 +- web/src/interfaces/AdminInterface.ts | 2 +- web/src/pages/tokens/TokenListPage.ts | 67 +++++++ web/src/routes.ts | 2 + 17 files changed, 285 insertions(+), 194 deletions(-) delete mode 100644 authentik/admin/templates/administration/token/list.html create mode 100644 web/src/pages/tokens/TokenListPage.ts diff --git a/authentik/admin/templates/administration/token/list.html b/authentik/admin/templates/administration/token/list.html deleted file mode 100644 index 9eb7667a4..000000000 --- a/authentik/admin/templates/administration/token/list.html +++ /dev/null @@ -1,102 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load authentik_utils %} - -{% block content %} -
    -
    -

    - - {% trans 'Tokens' %} -

    -

    {% trans "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access." %}

    -
    -
    -
    -
    - {% if object_list %} -
    -
    - {% include 'partials/toolbar_search.html' %} - {% include 'partials/pagination.html' %} -
    -
    - - - - - - - - - - - - {% for token in object_list %} - - - - - - - - {% endfor %} - -
    {% trans 'Identifier' %}{% trans 'User' %}{% trans 'Expires?' %}{% trans 'Expiry Date' %}
    -
    {{ token.identifier }}
    -
    - - {{ token.user }} - - - - {{ token.expiring|yesno:"Yes,No" }} - - - - {% if not token.expiring %} - - - {% else %} - {{ token.expires }} - {% endif %} - - - - - {% trans 'Delete' %} - -
    -
    - - {% trans 'Copy token' %} - -
    -
    - {% include 'partials/pagination.html' %} -
    - {% else %} -
    -
    - {% include 'partials/toolbar_search.html' %} -
    -
    -
    -
    - -

    - {% trans 'No Tokens.' %} -

    -
    - {% if request.GET.search != "" %} - {% trans "Your search query doesn't match any token." %} - {% else %} - {% trans 'Currently no tokens exist.' %} - {% endif %} -
    -
    -
    - {% endif %} -
    -
    -{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index dc6516f18..60596e00a 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -53,7 +53,6 @@ urlpatterns = [ name="application-delete", ), # Tokens - path("tokens/", tokens.TokenListView.as_view(), name="tokens"), path( "tokens//delete/", tokens.TokenDeleteView.as_view(), diff --git a/authentik/admin/views/tokens.py b/authentik/admin/views/tokens.py index 126dac064..0dc6ce311 100644 --- a/authentik/admin/views/tokens.py +++ b/authentik/admin/views/tokens.py @@ -1,39 +1,12 @@ """authentik Token administration""" from django.contrib.auth.mixins import LoginRequiredMixin -from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from django.views.generic import ListView -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import ( - DeleteMessageView, - SearchListMixin, - UserPaginateListMixin, -) +from authentik.admin.views.utils import DeleteMessageView from authentik.core.models import Token -class TokenListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - ListView, -): - """Show list of all tokens""" - - model = Token - permission_required = "authentik_core.view_token" - ordering = "expires" - template_name = "administration/token/list.html" - search_fields = [ - "identifier", - "intent", - "user__username", - "description", - ] - - class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView): """Delete token""" @@ -41,5 +14,5 @@ class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessage permission_required = "authentik_core.delete_token" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:tokens") + success_url = "/" success_message = _("Successfully deleted Token") diff --git a/authentik/admin/views/users.py b/authentik/admin/views/users.py index bc6d673cc..daea36fe9 100644 --- a/authentik/admin/views/users.py +++ b/authentik/admin/views/users.py @@ -7,19 +7,14 @@ from django.contrib.auth.mixins import ( from django.contrib.messages.views import SuccessMessageMixin from django.http import HttpRequest, HttpResponse from django.http.response import HttpResponseRedirect -from django.shortcuts import redirect +from django.shortcuts import redirect, reverse from django.utils.http import urlencode from django.utils.translation import gettext as _ from django.views.generic import DetailView, UpdateView -from guardian.mixins import ( - PermissionRequiredMixin, -) +from guardian.mixins import PermissionRequiredMixin from authentik.admin.forms.users import UserForm -from authentik.admin.views.utils import ( - BackSuccessUrlMixin, - DeleteMessageView, -) +from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView from authentik.core.models import Token, User from authentik.lib.views import CreateAssignPermView diff --git a/authentik/core/api/groups.py b/authentik/core/api/groups.py index fa1b8953d..152c1ea9f 100644 --- a/authentik/core/api/groups.py +++ b/authentik/core/api/groups.py @@ -19,3 +19,5 @@ class GroupViewSet(ModelViewSet): queryset = Group.objects.all() serializer_class = GroupSerializer + search_fields = ["name", "is_superuser"] + filterset_fields = ["name", "is_superuser"] diff --git a/authentik/core/api/tokens.py b/authentik/core/api/tokens.py index f30ba4f4b..aef414bff 100644 --- a/authentik/core/api/tokens.py +++ b/authentik/core/api/tokens.py @@ -9,6 +9,7 @@ from rest_framework.response import Response from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.viewsets import ModelViewSet +from authentik.core.api.users import UserSerializer from authentik.core.models import Token from authentik.events.models import Event, EventAction @@ -16,10 +17,21 @@ from authentik.events.models import Event, EventAction class TokenSerializer(ModelSerializer): """Token Serializer""" + user = UserSerializer() + class Meta: model = Token - fields = ["pk", "identifier", "intent", "user", "description"] + fields = [ + "pk", + "identifier", + "intent", + "user", + "description", + "expires", + "expiring", + ] + depth = 2 class TokenViewSerializer(Serializer): @@ -40,6 +52,19 @@ class TokenViewSet(ModelViewSet): lookup_field = "identifier" queryset = Token.filter_not_expired() serializer_class = TokenSerializer + search_fields = [ + "identifier", + "intent", + "user__username", + "description", + ] + filterset_fields = [ + "identifier", + "intent", + "user__username", + "description", + ] + ordering = ["expires"] @swagger_auto_schema(responses={200: TokenViewSerializer(many=False)}) @action(detail=True) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 1159b3a7d..573e04cd1 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -37,6 +37,7 @@ class UserSerializer(ModelSerializer): "is_superuser", "email", "avatar", + "attributes", ] @@ -45,6 +46,8 @@ class UserViewSet(ModelViewSet): queryset = User.objects.none() serializer_class = UserSerializer + search_fields = ["username", "name", "is_active"] + filterset_fields = ["username", "name", "is_active"] def get_queryset(self): return User.objects.all().exclude(pk=get_anonymous_user().pk) diff --git a/authentik/flows/api.py b/authentik/flows/api.py index 4ca0ff982..7a88df19e 100644 --- a/authentik/flows/api.py +++ b/authentik/flows/api.py @@ -186,6 +186,8 @@ class StageViewSet(ReadOnlyModelViewSet): queryset = Stage.objects.all() serializer_class = StageSerializer + search_fields = ["name"] + filterset_fields = ["name"] def get_queryset(self): return Stage.objects.select_subclasses() diff --git a/authentik/outposts/api/outpost_service_connections.py b/authentik/outposts/api/outpost_service_connections.py index 986002b4d..1a5c7d947 100644 --- a/authentik/outposts/api/outpost_service_connections.py +++ b/authentik/outposts/api/outpost_service_connections.py @@ -61,6 +61,8 @@ class ServiceConnectionViewSet(ModelViewSet): queryset = OutpostServiceConnection.objects.select_subclasses() serializer_class = ServiceConnectionSerializer + search_fields = ["name"] + filterset_fields = ["name"] @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)}) @action(detail=False) diff --git a/authentik/policies/api.py b/authentik/policies/api.py index 703d4bf0f..9b480d6b6 100644 --- a/authentik/policies/api.py +++ b/authentik/policies/api.py @@ -107,6 +107,7 @@ class PolicyViewSet(ReadOnlyModelViewSet): "bindings": ["isnull"], "promptstage": ["isnull"], } + search_fields = ["name"] def get_queryset(self): return Policy.objects.select_subclasses().prefetch_related( diff --git a/swagger.yaml b/swagger.yaml index d4d762204..2cc4d87bd 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -309,6 +309,16 @@ paths: operationId: core_groups_list description: Group Viewset parameters: + - name: name + in: query + description: '' + required: false + type: string + - name: is_superuser + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -436,6 +446,26 @@ paths: operationId: core_tokens_list description: Token Viewset parameters: + - name: identifier + in: query + description: '' + required: false + type: string + - name: intent + in: query + description: '' + required: false + type: string + - name: user__username + in: query + description: '' + required: false + type: string + - name: description + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -582,6 +612,21 @@ paths: operationId: core_users_list description: User Viewset parameters: + - name: username + in: query + description: '' + required: false + type: string + - name: name + in: query + description: '' + required: false + type: string + - name: is_active + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -649,6 +694,21 @@ paths: operationId: core_users_me description: Get information about current user parameters: + - name: username + in: query + description: '' + required: false + type: string + - name: name + in: query + description: '' + required: false + type: string + - name: is_active + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -2107,6 +2167,11 @@ paths: operationId: outposts_service_connections_all_list description: ServiceConnection Viewset parameters: + - name: name + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -2174,6 +2239,11 @@ paths: operationId: outposts_service_connections_all_types description: Get all creatable service connection types parameters: + - name: name + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -5506,6 +5576,11 @@ paths: operationId: stages_all_list description: Stage Viewset parameters: + - name: name + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -5557,6 +5632,11 @@ paths: operationId: stages_all_types description: Get all creatable stage types parameters: + - name: name + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -8091,48 +8171,8 @@ definitions: attributes: title: Attributes type: object - Token: - description: Token Serializer - required: - - identifier - - user - type: object - properties: - pk: - title: Token uuid - type: string - format: uuid - readOnly: true - identifier: - title: Identifier - type: string - format: slug - pattern: ^[-a-zA-Z0-9_]+$ - maxLength: 255 - minLength: 1 - intent: - title: Intent - type: string - enum: - - verification - - api - - recovery - user: - title: User - type: integer - description: - title: Description - type: string - TokenView: - description: Show token's current key - type: object - properties: - key: - title: Key - type: string - readOnly: true - minLength: 1 User: + title: User description: User Serializer required: - username @@ -8179,6 +8219,56 @@ definitions: title: Avatar type: string readOnly: true + attributes: + title: Attributes + type: object + Token: + description: Token Serializer + required: + - identifier + - user + type: object + properties: + pk: + title: Token uuid + type: string + format: uuid + readOnly: true + identifier: + title: Identifier + type: string + format: slug + pattern: ^[-a-zA-Z0-9_]+$ + maxLength: 255 + minLength: 1 + intent: + title: Intent + type: string + enum: + - verification + - api + - recovery + user: + $ref: '#/definitions/User' + description: + title: Description + type: string + expires: + title: Expires + type: string + format: date-time + expiring: + title: Expiring + type: boolean + TokenView: + description: Show token's current key + type: object + properties: + key: + title: Key + type: string + readOnly: true + minLength: 1 CertificateKeyPair: description: CertificateKeyPair Serializer required: diff --git a/web/src/api/Tokens.ts b/web/src/api/Tokens.ts index 7a5e75d49..e1c6ab1b0 100644 --- a/web/src/api/Tokens.ts +++ b/web/src/api/Tokens.ts @@ -1,11 +1,43 @@ -import { DefaultClient } from "./Client"; +import { AKResponse, DefaultClient, QueryArguments } from "./Client"; +import { User } from "./Users"; -interface TokenResponse { - key: string; +export enum TokenIntent { + INTENT_VERIFICATION = "verification", + INTENT_API = "api", + INTENT_RECOVERY = "recovery", } -export function tokenByIdentifier(identifier: string): Promise { - return DefaultClient.fetch(["core", "tokens", identifier, "view_key"]).then( - (r) => r.key - ); +export class Token { + + pk: string; + identifier: string; + intent: TokenIntent; + user: User; + description: string; + + expires: number; + expiring: boolean; + + constructor() { + throw Error(); + } + + static get(pk: string): Promise { + return DefaultClient.fetch(["core", "tokens", pk]); + } + + static list(filter?: QueryArguments): Promise> { + return DefaultClient.fetch>(["core", "tokens"], filter); + } + + static adminUrl(rest: string): string { + return `/administration/tokens/${rest}`; + } + + static getKey(identifier: string): Promise { + return DefaultClient.fetch<{ key: string }>(["core", "tokens", identifier, "view_key"]).then( + (r) => r.key + ); + } + } diff --git a/web/src/elements/buttons/TokenCopyButton.ts b/web/src/elements/buttons/TokenCopyButton.ts index 5b68818e0..075469812 100644 --- a/web/src/elements/buttons/TokenCopyButton.ts +++ b/web/src/elements/buttons/TokenCopyButton.ts @@ -3,7 +3,7 @@ import { css, CSSResult, customElement, html, LitElement, property, TemplateResu import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css"; // @ts-ignore import ButtonStyle from "@patternfly/patternfly/components/Button/button.css"; -import { tokenByIdentifier } from "../../api/Tokens"; +import { Token } from "../../api/Tokens"; import { ColorStyles, ERROR_CLASS, PRIMARY_CLASS, SUCCESS_CLASS } from "../../constants"; @customElement("ak-token-copy-button") @@ -35,7 +35,7 @@ export class TokenCopyButton extends LitElement { }, 1500); return; } - tokenByIdentifier(this.identifier).then((token) => { + Token.getKey(this.identifier).then((token) => { navigator.clipboard.writeText(token).then(() => { this.buttonClass = SUCCESS_CLASS; setTimeout(() => { diff --git a/web/src/elements/table/TablePagination.ts b/web/src/elements/table/TablePagination.ts index 5ceea8604..fbcae2f27 100644 --- a/web/src/elements/table/TablePagination.ts +++ b/web/src/elements/table/TablePagination.ts @@ -43,7 +43,7 @@ export class TablePagination extends LitElement { - - - - - {% include 'partials/pagination.html' %} - - - - - - - - - - - - {% for stage in object_list %} - - - - - - {% endfor %} - -
    {% trans 'Name' %}{% trans 'Flows' %}
    -
    -
    {{ stage.name }}
    - {{ stage|verbose_name }} -
    -
    -
      - {% for flow in stage.flow_set.all %} -
    • {{ flow.slug }}
    • - {% empty %} -
    • -
    • - {% endfor %} -
    -
    - - - {% trans 'Edit' %} - -
    -
    - - - {% trans 'Delete' %} - -
    -
    -
    -
    - {% include 'partials/pagination.html' %} -
    - {% else %} -
    -
    - {% include 'partials/toolbar_search.html' %} -
    -
    -
    -
    - -

    - {% trans 'No Stages.' %} -

    -
    - {% if request.GET.search != "" %} - {% trans "Your search query doesn't match any stages." %} - {% else %} - {% trans 'Currently no stages exist. Click the button below to create one.' %} - {% endif %} -
    - - - - -
    -
    - {% endif %} - - -{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index 60596e00a..2ac5b256b 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -125,7 +125,6 @@ urlpatterns = [ name="provider-delete", ), # Stages - path("stages/", stages.StageListView.as_view(), name="stages"), path("stages/create/", stages.StageCreateView.as_view(), name="stage-create"), path( "stages//update/", diff --git a/authentik/admin/views/stages.py b/authentik/admin/views/stages.py index 55e7623dd..bb4b190eb 100644 --- a/authentik/admin/views/stages.py +++ b/authentik/admin/views/stages.py @@ -4,38 +4,18 @@ from django.contrib.auth.mixins import ( PermissionRequiredMixin as DjangoPermissionRequiredMixin, ) from django.contrib.messages.views import SuccessMessageMixin -from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from guardian.mixins import PermissionRequiredMixin from authentik.admin.views.utils import ( BackSuccessUrlMixin, DeleteMessageView, InheritanceCreateView, - InheritanceListView, InheritanceUpdateView, - SearchListMixin, - UserPaginateListMixin, ) from authentik.flows.models import Stage -class StageListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - InheritanceListView, -): - """Show list of all stages""" - - model = Stage - template_name = "administration/stage/list.html" - permission_required = "authentik_flows.view_stage" - ordering = "name" - search_fields = ["name"] - - class StageCreateView( SuccessMessageMixin, BackSuccessUrlMixin, @@ -49,7 +29,7 @@ class StageCreateView( template_name = "generic/create.html" permission_required = "authentik_flows.add_stage" - success_url = reverse_lazy("authentik_admin:stages") + success_url = "/" success_message = _("Successfully created Stage") @@ -65,7 +45,7 @@ class StageUpdateView( model = Stage permission_required = "authentik_flows.update_application" template_name = "generic/update.html" - success_url = reverse_lazy("authentik_admin:stages") + success_url = "/" success_message = _("Successfully updated Stage") @@ -75,5 +55,5 @@ class StageDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessage model = Stage template_name = "generic/delete.html" permission_required = "authentik_flows.delete_stage" - success_url = reverse_lazy("authentik_admin:stages") + success_url = "/" success_message = _("Successfully deleted Stage") diff --git a/authentik/flows/api/stages.py b/authentik/flows/api/stages.py index 000294cf7..56ba52937 100644 --- a/authentik/flows/api/stages.py +++ b/authentik/flows/api/stages.py @@ -8,8 +8,8 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField from rest_framework.viewsets import ReadOnlyModelViewSet from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer +from authentik.flows.api.flows import FlowSerializer from authentik.flows.models import Stage -from authentik.flows.planner import cache_key from authentik.lib.templatetags.authentik_utils import verbose_name from authentik.lib.utils.reflection import all_subclasses @@ -18,6 +18,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer): """Stage Serializer""" object_type = SerializerMethodField() + flow_set = FlowSerializer(many=True) def get_object_type(self, obj: Stage) -> str: """Get object type so that we know which API Endpoint to use to get the full object""" @@ -26,13 +27,20 @@ class StageSerializer(ModelSerializer, MetaNameSerializer): class Meta: model = Stage - fields = ["pk", "name", "object_type", "verbose_name", "verbose_name_plural"] + fields = [ + "pk", + "name", + "object_type", + "verbose_name", + "verbose_name_plural", + "flow_set", + ] class StageViewSet(ReadOnlyModelViewSet): """Stage Viewset""" - queryset = Stage.objects.all() + queryset = Stage.objects.all().select_related("flow_set") serializer_class = StageSerializer search_fields = ["name"] filterset_fields = ["name"] diff --git a/web/src/api/Flows.ts b/web/src/api/Flows.ts index 1b1af9825..cbf4909d5 100644 --- a/web/src/api/Flows.ts +++ b/web/src/api/Flows.ts @@ -1,4 +1,4 @@ -import { DefaultClient, AKResponse, QueryArguments } from "./Client"; +import { DefaultClient, AKResponse, QueryArguments, BaseInheritanceModel } from "./Client"; import { TypeCreate } from "./Providers"; export enum FlowDesignation { @@ -49,16 +49,26 @@ export class Flow { } } -export class Stage { +export class Stage implements BaseInheritanceModel { pk: string; name: string; - __type__: string; + object_type: string; verbose_name: string; + verbose_name_plural: string; + flow_set: Flow[]; constructor() { throw Error(); } + static get(slug: string): Promise { + return DefaultClient.fetch(["stages", "all", slug]); + } + + static list(filter?: QueryArguments): Promise> { + return DefaultClient.fetch>(["stages", "all"], filter); + } + static getTypes(): Promise { return DefaultClient.fetch(["stages", "all", "types"]); } diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index af881bee1..704a212d3 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -20,37 +20,37 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ return User.me().then(u => u.is_superuser); }), new SidebarItem("Resources").children( - new SidebarItem("Applications", "/applications").activeWhen( - `^/applications/(?${SLUG_REGEX})$` + new SidebarItem("Applications", "/core/applications").activeWhen( + `^/core/applications/(?${SLUG_REGEX})$` ), - new SidebarItem("Sources", "/sources").activeWhen( - `^/sources/(?${SLUG_REGEX})$`, + new SidebarItem("Sources", "/core/sources").activeWhen( + `^/core/sources/(?${SLUG_REGEX})$`, ), - new SidebarItem("Providers", "/providers"), - new SidebarItem("Outposts", "/outposts"), - new SidebarItem("Outpost Service Connections", "/outpost-service-connections"), + new SidebarItem("Providers", "/core/providers"), + new SidebarItem("Outposts", "/outpost/outposts"), + new SidebarItem("Outpost Service Connections", "/outpost/service-connections"), ).when((): Promise => { return User.me().then(u => u.is_superuser); }), new SidebarItem("Customisation").children( - new SidebarItem("Policies", "/policies"), - new SidebarItem("Property Mappings", "/property-mappings"), + new SidebarItem("Policies", "/policy/policies"), + new SidebarItem("Property Mappings", "/core/property-mappings"), ).when((): Promise => { return User.me().then(u => u.is_superuser); }), new SidebarItem("Flows").children( - new SidebarItem("Flows", "/flows").activeWhen(`^/flows/(?${SLUG_REGEX})$`), - new SidebarItem("Stages", "/administration/stages/"), + new SidebarItem("Flows", "/flow/flows").activeWhen(`^/flow/flows/(?${SLUG_REGEX})$`), + new SidebarItem("Stages", "/flow/stages"), new SidebarItem("Prompts", "/administration/stages_prompts/"), new SidebarItem("Invitations", "/administration/stages/invitations/"), ).when((): Promise => { return User.me().then(u => u.is_superuser); }), new SidebarItem("Identity & Cryptography").children( - new SidebarItem("User", "/users"), - new SidebarItem("Groups", "/groups"), + new SidebarItem("User", "/identity/users"), + new SidebarItem("Groups", "/identity/groups"), new SidebarItem("Certificates", "/crypto/certificates"), - new SidebarItem("Tokens", "/tokens"), + new SidebarItem("Tokens", "/core/tokens"), ).when((): Promise => { return User.me().then(u => u.is_superuser); }), diff --git a/web/src/pages/admin-overview/AdminOverviewPage.ts b/web/src/pages/admin-overview/AdminOverviewPage.ts index b825f8d0e..00292009a 100644 --- a/web/src/pages/admin-overview/AdminOverviewPage.ts +++ b/web/src/pages/admin-overview/AdminOverviewPage.ts @@ -36,7 +36,7 @@ export class AdminOverviewPage extends LitElement { - + diff --git a/web/src/pages/applications/ApplicationListPage.ts b/web/src/pages/applications/ApplicationListPage.ts index 29738d8ec..608670c58 100644 --- a/web/src/pages/applications/ApplicationListPage.ts +++ b/web/src/pages/applications/ApplicationListPage.ts @@ -50,7 +50,7 @@ export class ApplicationListPage extends TablePage { item.meta_icon ? html`${gettext(` : html``, - html` + html`
    ${item.name}
    diff --git a/web/src/pages/applications/ApplicationViewPage.ts b/web/src/pages/applications/ApplicationViewPage.ts index 41065bc41..3b38bae6a 100644 --- a/web/src/pages/applications/ApplicationViewPage.ts +++ b/web/src/pages/applications/ApplicationViewPage.ts @@ -80,7 +80,7 @@ export class ApplicationViewPage extends LitElement {
    diff --git a/web/src/pages/events/EventInfo.ts b/web/src/pages/events/EventInfo.ts index df8c384e8..50a2c12f0 100644 --- a/web/src/pages/events/EventInfo.ts +++ b/web/src/pages/events/EventInfo.ts @@ -107,7 +107,7 @@ export class EventInfo extends LitElement { ${until(Flow.list({ flow_uuid: this.event.context.flow as string, }).then(resp => { - return html`${resp.results[0].name}`; + return html`${resp.results[0].name}`; }), html``)} diff --git a/web/src/pages/flows/FlowListPage.ts b/web/src/pages/flows/FlowListPage.ts index 9689ccd3d..b7ae5a90f 100644 --- a/web/src/pages/flows/FlowListPage.ts +++ b/web/src/pages/flows/FlowListPage.ts @@ -47,7 +47,7 @@ export class FlowListPage extends TablePage { row(item: Flow): TemplateResult[] { return [ - html` + html` ${item.slug} `, html`${item.name}`, diff --git a/web/src/pages/outposts/OutpostListPage.ts b/web/src/pages/outposts/OutpostListPage.ts index 71b636028..81e15e027 100644 --- a/web/src/pages/outposts/OutpostListPage.ts +++ b/web/src/pages/outposts/OutpostListPage.ts @@ -48,7 +48,7 @@ export class OutpostListPage extends TablePage { return [ html`${item.name}`, html``, html``, html` diff --git a/web/src/pages/providers/ProviderListPage.ts b/web/src/pages/providers/ProviderListPage.ts index d0bc61e6f..fe6ed3bab 100644 --- a/web/src/pages/providers/ProviderListPage.ts +++ b/web/src/pages/providers/ProviderListPage.ts @@ -47,13 +47,13 @@ export class ProviderListPage extends TablePage { row(item: Provider): TemplateResult[] { return [ - html` + html` ${item.name} `, item.assigned_application_name ? html` ${gettext("Assigned to application ")} - ${item.assigned_application_name}` : + ${item.assigned_application_name}` : html` ${gettext("Warning: Provider not assigned to any application.")}`, html`${item.verbose_name}`, diff --git a/web/src/pages/providers/RelatedApplicationButton.ts b/web/src/pages/providers/RelatedApplicationButton.ts index 5abe601a3..e7194c8a2 100644 --- a/web/src/pages/providers/RelatedApplicationButton.ts +++ b/web/src/pages/providers/RelatedApplicationButton.ts @@ -14,7 +14,7 @@ export class RelatedApplicationButton extends LitElement { render(): TemplateResult { if (this.provider?.assigned_application_slug) { - return html` + return html` ${this.provider.assigned_application_name} `; } diff --git a/web/src/pages/sources/SourcesListPage.ts b/web/src/pages/sources/SourcesListPage.ts index 30511aa4a..76e9b9410 100644 --- a/web/src/pages/sources/SourcesListPage.ts +++ b/web/src/pages/sources/SourcesListPage.ts @@ -46,7 +46,7 @@ export class SourceListPage extends TablePage { row(item: Source): TemplateResult[] { return [ - html` + html`
    ${item.name}
    ${item.enabled ? html`` : html`${gettext("Disabled")}`}
    `, diff --git a/web/src/pages/stages/StageListPage.ts b/web/src/pages/stages/StageListPage.ts new file mode 100644 index 000000000..710512a1d --- /dev/null +++ b/web/src/pages/stages/StageListPage.ts @@ -0,0 +1,100 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { AKResponse } from "../../api/Client"; +import { TableColumn } from "../../elements/table/Table"; +import { TablePage } from "../../elements/table/TablePage"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/SpinnerButton"; +import "../../elements/buttons/Dropdown"; +import { until } from "lit-html/directives/until"; +import { Stage } from "../../api/Flows"; + +@customElement("ak-stage-list") +export class StageListPage extends TablePage { + pageTitle(): string { + return "Stages"; + } + pageDescription(): string | undefined { + return "Stages are single steps of a Flow that a user is guided through."; + } + pageIcon(): string { + return "pf-icon pf-icon-plugged"; + } + searchEnabled(): boolean { + return true; + } + + @property() + order = "name"; + + apiEndpoint(page: number): Promise> { + return Stage.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn("Name", "name"), + new TableColumn("Flows"), + new TableColumn(""), + ]; + } + + row(item: Stage): TemplateResult[] { + return [ + html`
    +
    ${item.name}
    + ${item.verbose_name} +
    `, + html`${item.flow_set.map((flow) => { + return html` + ${flow.slug} + `; + })}`, + html` + + + ${gettext("Edit")} + +
    +
    + + + ${gettext("Delete")} + +
    +
    + `, + ]; + } + + renderToolbar(): TemplateResult { + return html` + + + + + ${super.renderToolbar()}`; + } + +} diff --git a/web/src/routes.ts b/web/src/routes.ts index 32f249ce2..c33ce2f76 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -11,6 +11,7 @@ import "./pages/events/RuleListPage"; import "./pages/events/TransportListPage"; import "./pages/flows/FlowListPage"; import "./pages/flows/FlowViewPage"; +import "./pages/groups/GroupListPage"; import "./pages/LibraryPage"; import "./pages/outposts/OutpostListPage"; import "./pages/outposts/OutpostServiceConnectionListPage"; @@ -20,10 +21,10 @@ import "./pages/providers/ProviderListPage"; import "./pages/providers/ProviderViewPage"; import "./pages/sources/SourcesListPage"; import "./pages/sources/SourceViewPage"; -import "./pages/groups/GroupListPage"; -import "./pages/users/UserListPage"; -import "./pages/tokens/TokenListPage"; +import "./pages/stages/StageListPage"; import "./pages/system-tasks/SystemTaskListPage"; +import "./pages/tokens/TokenListPage"; +import "./pages/users/UserListPage"; export const ROUTES: Route[] = [ // Prevent infinite Shell loops @@ -32,24 +33,25 @@ export const ROUTES: Route[] = [ new Route(new RegExp("^/library$"), html``), new Route(new RegExp("^/administration/overview$"), html``), new Route(new RegExp("^/administration/system-tasks$"), html``), - new Route(new RegExp("^/providers$"), html``), - new Route(new RegExp(`^/providers/(?${ID_REGEX})$`)).then((args) => { + new Route(new RegExp("^/core/providers$"), html``), + new Route(new RegExp(`^/core/providers/(?${ID_REGEX})$`)).then((args) => { return html``; }), - new Route(new RegExp("^/applications$"), html``), - new Route(new RegExp(`^/applications/(?${SLUG_REGEX})$`)).then((args) => { + new Route(new RegExp("^/core/applications$"), html``), + new Route(new RegExp(`^/core/applications/(?${SLUG_REGEX})$`)).then((args) => { return html``; }), - new Route(new RegExp("^/sources$"), html``), - new Route(new RegExp(`^/sources/(?${SLUG_REGEX})$`)).then((args) => { + new Route(new RegExp("^/core/sources$"), html``), + new Route(new RegExp(`^/core/sources/(?${SLUG_REGEX})$`)).then((args) => { return html``; }), - new Route(new RegExp("^/policies$"), html``), - new Route(new RegExp("^/groups$"), html``), - new Route(new RegExp("^/users$"), html``), - new Route(new RegExp("^/flows$"), html``), - new Route(new RegExp("^/tokens$"), html``), - new Route(new RegExp(`^/flows/(?${SLUG_REGEX})$`)).then((args) => { + new Route(new RegExp("^/policy/policies$"), html``), + new Route(new RegExp("^/identity/groups$"), html``), + new Route(new RegExp("^/identity/users$"), html``), + new Route(new RegExp("^/core/tokens$"), html``), + new Route(new RegExp("^/flow/stages$"), html``), + new Route(new RegExp("^/flow/flows$"), html``), + new Route(new RegExp(`^/flow/flows/(?${SLUG_REGEX})$`)).then((args) => { return html``; }), new Route(new RegExp("^/events/log$"), html``), @@ -58,8 +60,8 @@ export const ROUTES: Route[] = [ }), new Route(new RegExp("^/events/transports$"), html``), new Route(new RegExp("^/events/rules$"), html``), - new Route(new RegExp("^/property-mappings$"), html``), - new Route(new RegExp("^/outposts$"), html``), - new Route(new RegExp("^/outpost-service-connections$"), html``), + new Route(new RegExp("^/core/property-mappings$"), html``), + new Route(new RegExp("^/outpost/outposts$"), html``), + new Route(new RegExp("^/outpost/service-connections$"), html``), new Route(new RegExp("^/crypto/certificates$"), html``), ]; From 9b12895faba448b161fdc53552d98bd52498d432 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 19:31:23 +0100 Subject: [PATCH 29/34] admin: remove unneeded code --- authentik/admin/views/applications.py | 4 +-- authentik/admin/views/certificate_key_pair.py | 5 +-- .../admin/views/events_notifications_rules.py | 4 +-- .../views/events_notifications_transports.py | 4 +-- authentik/admin/views/flows.py | 4 +-- authentik/admin/views/groups.py | 4 +-- authentik/admin/views/outposts.py | 4 +-- .../views/outposts_service_connections.py | 3 -- authentik/admin/views/policies.py | 3 -- authentik/admin/views/policies_bindings.py | 4 +-- authentik/admin/views/property_mappings.py | 3 -- authentik/admin/views/providers.py | 3 -- authentik/admin/views/sources.py | 3 -- authentik/admin/views/stages.py | 3 -- authentik/admin/views/stages_bindings.py | 4 +-- authentik/admin/views/stages_invitations.py | 2 -- authentik/admin/views/stages_prompts.py | 3 -- authentik/admin/views/users.py | 15 +++------ authentik/admin/views/utils.py | 33 ++----------------- 19 files changed, 15 insertions(+), 93 deletions(-) diff --git a/authentik/admin/views/applications.py b/authentik/admin/views/applications.py index 88e72940c..f80eda369 100644 --- a/authentik/admin/views/applications.py +++ b/authentik/admin/views/applications.py @@ -11,7 +11,7 @@ from django.views.generic import UpdateView from guardian.mixins import PermissionRequiredMixin from guardian.shortcuts import get_objects_for_user -from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView +from authentik.admin.views.utils import DeleteMessageView from authentik.core.forms.applications import ApplicationForm from authentik.core.models import Application from authentik.lib.views import CreateAssignPermView @@ -19,7 +19,6 @@ from authentik.lib.views import CreateAssignPermView class ApplicationCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, @@ -52,7 +51,6 @@ class ApplicationCreateView( class ApplicationUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView, diff --git a/authentik/admin/views/certificate_key_pair.py b/authentik/admin/views/certificate_key_pair.py index 4653cb154..1e2ea400a 100644 --- a/authentik/admin/views/certificate_key_pair.py +++ b/authentik/admin/views/certificate_key_pair.py @@ -10,7 +10,7 @@ from django.views.generic import UpdateView from django.views.generic.edit import FormView from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView +from authentik.admin.views.utils import DeleteMessageView from authentik.crypto.builder import CertificateBuilder from authentik.crypto.forms import ( CertificateKeyPairForm, @@ -22,7 +22,6 @@ from authentik.lib.views import CreateAssignPermView class CertificateKeyPairCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, @@ -40,7 +39,6 @@ class CertificateKeyPairCreateView( class CertificateKeyPairGenerateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, FormView, @@ -68,7 +66,6 @@ class CertificateKeyPairGenerateView( class CertificateKeyPairUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView, diff --git a/authentik/admin/views/events_notifications_rules.py b/authentik/admin/views/events_notifications_rules.py index 7cb2fc390..66577dc40 100644 --- a/authentik/admin/views/events_notifications_rules.py +++ b/authentik/admin/views/events_notifications_rules.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext as _ from django.views.generic import UpdateView from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView +from authentik.admin.views.utils import DeleteMessageView from authentik.events.forms import NotificationRuleForm from authentik.events.models import NotificationRule from authentik.lib.views import CreateAssignPermView @@ -16,7 +16,6 @@ from authentik.lib.views import CreateAssignPermView class NotificationRuleCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, @@ -33,7 +32,6 @@ class NotificationRuleCreateView( class NotificationRuleUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView, diff --git a/authentik/admin/views/events_notifications_transports.py b/authentik/admin/views/events_notifications_transports.py index d211831ed..a5c4cd990 100644 --- a/authentik/admin/views/events_notifications_transports.py +++ b/authentik/admin/views/events_notifications_transports.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext as _ from django.views.generic import UpdateView from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView +from authentik.admin.views.utils import DeleteMessageView from authentik.events.forms import NotificationTransportForm from authentik.events.models import NotificationTransport from authentik.lib.views import CreateAssignPermView @@ -16,7 +16,6 @@ from authentik.lib.views import CreateAssignPermView class NotificationTransportCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, @@ -33,7 +32,6 @@ class NotificationTransportCreateView( class NotificationTransportUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView, diff --git a/authentik/admin/views/flows.py b/authentik/admin/views/flows.py index 775316245..9cfa6ffd2 100644 --- a/authentik/admin/views/flows.py +++ b/authentik/admin/views/flows.py @@ -10,7 +10,7 @@ from django.utils.translation import gettext as _ from django.views.generic import DetailView, FormView, UpdateView from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView +from authentik.admin.views.utils import DeleteMessageView from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.forms import FlowForm, FlowImportForm from authentik.flows.models import Flow @@ -25,7 +25,6 @@ from authentik.lib.views import CreateAssignPermView, bad_request_message class FlowCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, @@ -43,7 +42,6 @@ class FlowCreateView( class FlowUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView, diff --git a/authentik/admin/views/groups.py b/authentik/admin/views/groups.py index 462fd87ba..74a77b5f1 100644 --- a/authentik/admin/views/groups.py +++ b/authentik/admin/views/groups.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext as _ from django.views.generic import UpdateView from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView +from authentik.admin.views.utils import DeleteMessageView from authentik.core.forms.groups import GroupForm from authentik.core.models import Group from authentik.lib.views import CreateAssignPermView @@ -16,7 +16,6 @@ from authentik.lib.views import CreateAssignPermView class GroupCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, @@ -34,7 +33,6 @@ class GroupCreateView( class GroupUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView, diff --git a/authentik/admin/views/outposts.py b/authentik/admin/views/outposts.py index 5cc7bd8eb..fc160b901 100644 --- a/authentik/admin/views/outposts.py +++ b/authentik/admin/views/outposts.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext as _ from django.views.generic import UpdateView from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView +from authentik.admin.views.utils import DeleteMessageView from authentik.lib.views import CreateAssignPermView from authentik.outposts.forms import OutpostForm from authentik.outposts.models import Outpost, OutpostConfig @@ -19,7 +19,6 @@ from authentik.outposts.models import Outpost, OutpostConfig class OutpostCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, @@ -43,7 +42,6 @@ class OutpostCreateView( class OutpostUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView, diff --git a/authentik/admin/views/outposts_service_connections.py b/authentik/admin/views/outposts_service_connections.py index 4f2b107ed..442690bed 100644 --- a/authentik/admin/views/outposts_service_connections.py +++ b/authentik/admin/views/outposts_service_connections.py @@ -8,7 +8,6 @@ from django.utils.translation import gettext as _ from guardian.mixins import PermissionRequiredMixin from authentik.admin.views.utils import ( - BackSuccessUrlMixin, DeleteMessageView, InheritanceCreateView, InheritanceUpdateView, @@ -18,7 +17,6 @@ from authentik.outposts.models import OutpostServiceConnection class OutpostServiceConnectionCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, InheritanceCreateView, @@ -35,7 +33,6 @@ class OutpostServiceConnectionCreateView( class OutpostServiceConnectionUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, InheritanceUpdateView, diff --git a/authentik/admin/views/policies.py b/authentik/admin/views/policies.py index d4491edf2..bb18a3502 100644 --- a/authentik/admin/views/policies.py +++ b/authentik/admin/views/policies.py @@ -14,7 +14,6 @@ from guardian.mixins import PermissionRequiredMixin from authentik.admin.forms.policies import PolicyTestForm from authentik.admin.views.utils import ( - BackSuccessUrlMixin, DeleteMessageView, InheritanceCreateView, InheritanceUpdateView, @@ -25,7 +24,6 @@ from authentik.policies.process import PolicyProcess, PolicyRequest class PolicyCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, InheritanceCreateView, @@ -42,7 +40,6 @@ class PolicyCreateView( class PolicyUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, InheritanceUpdateView, diff --git a/authentik/admin/views/policies_bindings.py b/authentik/admin/views/policies_bindings.py index 3dfd8ca9a..336f0dd64 100644 --- a/authentik/admin/views/policies_bindings.py +++ b/authentik/admin/views/policies_bindings.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext as _ from django.views.generic import UpdateView from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView +from authentik.admin.views.utils import DeleteMessageView from authentik.lib.views import CreateAssignPermView from authentik.policies.forms import PolicyBindingForm from authentik.policies.models import PolicyBinding, PolicyBindingModel @@ -19,7 +19,6 @@ from authentik.policies.models import PolicyBinding, PolicyBindingModel class PolicyBindingCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, @@ -53,7 +52,6 @@ class PolicyBindingCreateView( class PolicyBindingUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView, diff --git a/authentik/admin/views/property_mappings.py b/authentik/admin/views/property_mappings.py index ca6b2796f..f9b2fc7d7 100644 --- a/authentik/admin/views/property_mappings.py +++ b/authentik/admin/views/property_mappings.py @@ -15,7 +15,6 @@ from guardian.mixins import PermissionRequiredMixin from authentik.admin.forms.policies import PolicyTestForm from authentik.admin.views.utils import ( - BackSuccessUrlMixin, DeleteMessageView, InheritanceCreateView, InheritanceUpdateView, @@ -25,7 +24,6 @@ from authentik.core.models import PropertyMapping class PropertyMappingCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, InheritanceCreateView, @@ -41,7 +39,6 @@ class PropertyMappingCreateView( class PropertyMappingUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, InheritanceUpdateView, diff --git a/authentik/admin/views/providers.py b/authentik/admin/views/providers.py index 76eefa839..71394c18a 100644 --- a/authentik/admin/views/providers.py +++ b/authentik/admin/views/providers.py @@ -8,7 +8,6 @@ from django.utils.translation import gettext as _ from guardian.mixins import PermissionRequiredMixin from authentik.admin.views.utils import ( - BackSuccessUrlMixin, DeleteMessageView, InheritanceCreateView, InheritanceUpdateView, @@ -18,7 +17,6 @@ from authentik.core.models import Provider class ProviderCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, InheritanceCreateView, @@ -34,7 +32,6 @@ class ProviderCreateView( class ProviderUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, InheritanceUpdateView, diff --git a/authentik/admin/views/sources.py b/authentik/admin/views/sources.py index 7ebe243b8..5bc002ced 100644 --- a/authentik/admin/views/sources.py +++ b/authentik/admin/views/sources.py @@ -8,7 +8,6 @@ from django.utils.translation import gettext as _ from guardian.mixins import PermissionRequiredMixin from authentik.admin.views.utils import ( - BackSuccessUrlMixin, DeleteMessageView, InheritanceCreateView, InheritanceUpdateView, @@ -18,7 +17,6 @@ from authentik.core.models import Source class SourceCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, InheritanceCreateView, @@ -34,7 +32,6 @@ class SourceCreateView( class SourceUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, InheritanceUpdateView, diff --git a/authentik/admin/views/stages.py b/authentik/admin/views/stages.py index bb4b190eb..049b591e4 100644 --- a/authentik/admin/views/stages.py +++ b/authentik/admin/views/stages.py @@ -8,7 +8,6 @@ from django.utils.translation import gettext as _ from guardian.mixins import PermissionRequiredMixin from authentik.admin.views.utils import ( - BackSuccessUrlMixin, DeleteMessageView, InheritanceCreateView, InheritanceUpdateView, @@ -18,7 +17,6 @@ from authentik.flows.models import Stage class StageCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, InheritanceCreateView, @@ -35,7 +33,6 @@ class StageCreateView( class StageUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, InheritanceUpdateView, diff --git a/authentik/admin/views/stages_bindings.py b/authentik/admin/views/stages_bindings.py index 7aae1d33c..e8376e6b3 100644 --- a/authentik/admin/views/stages_bindings.py +++ b/authentik/admin/views/stages_bindings.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext as _ from django.views.generic import UpdateView from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView +from authentik.admin.views.utils import DeleteMessageView from authentik.flows.forms import FlowStageBindingForm from authentik.flows.models import Flow, FlowStageBinding from authentik.lib.views import CreateAssignPermView @@ -19,7 +19,6 @@ from authentik.lib.views import CreateAssignPermView class StageBindingCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, @@ -51,7 +50,6 @@ class StageBindingCreateView( class StageBindingUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView, diff --git a/authentik/admin/views/stages_invitations.py b/authentik/admin/views/stages_invitations.py index cf2c4bcca..8c6e0aa81 100644 --- a/authentik/admin/views/stages_invitations.py +++ b/authentik/admin/views/stages_invitations.py @@ -11,7 +11,6 @@ from django.views.generic import ListView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from authentik.admin.views.utils import ( - BackSuccessUrlMixin, DeleteMessageView, SearchListMixin, UserPaginateListMixin, @@ -39,7 +38,6 @@ class InvitationListView( class InvitationCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, diff --git a/authentik/admin/views/stages_prompts.py b/authentik/admin/views/stages_prompts.py index cc59a2ba5..e1f182336 100644 --- a/authentik/admin/views/stages_prompts.py +++ b/authentik/admin/views/stages_prompts.py @@ -10,7 +10,6 @@ from django.views.generic import ListView, UpdateView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from authentik.admin.views.utils import ( - BackSuccessUrlMixin, DeleteMessageView, SearchListMixin, UserPaginateListMixin, @@ -43,7 +42,6 @@ class PromptListView( class PromptCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, @@ -61,7 +59,6 @@ class PromptCreateView( class PromptUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView, diff --git a/authentik/admin/views/users.py b/authentik/admin/views/users.py index daea36fe9..379a8c641 100644 --- a/authentik/admin/views/users.py +++ b/authentik/admin/views/users.py @@ -14,14 +14,13 @@ from django.views.generic import DetailView, UpdateView from guardian.mixins import PermissionRequiredMixin from authentik.admin.forms.users import UserForm -from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView +from authentik.admin.views.utils import DeleteMessageView from authentik.core.models import Token, User from authentik.lib.views import CreateAssignPermView class UserCreateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, @@ -39,7 +38,6 @@ class UserCreateView( class UserUpdateView( SuccessMessageMixin, - BackSuccessUrlMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView, @@ -70,9 +68,7 @@ class UserDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageV success_message = _("Successfully deleted User") -class UserDisableView( - LoginRequiredMixin, PermissionRequiredMixin, BackSuccessUrlMixin, DeleteMessageView -): +class UserDisableView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView): """Disable user""" object: User @@ -94,9 +90,7 @@ class UserDisableView( return HttpResponseRedirect(success_url) -class UserEnableView( - LoginRequiredMixin, PermissionRequiredMixin, BackSuccessUrlMixin, DetailView -): +class UserEnableView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """Enable user""" object: User @@ -111,10 +105,9 @@ class UserEnableView( def get(self, request: HttpRequest, *args, **kwargs): self.object: User = self.get_object() - success_url = self.get_success_url() self.object.is_active = True self.object.save() - return HttpResponseRedirect(success_url) + return HttpResponseRedirect(self.success_url) class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): diff --git a/authentik/admin/views/utils.py b/authentik/admin/views/utils.py index 17c33f434..8146a4607 100644 --- a/authentik/admin/views/utils.py +++ b/authentik/admin/views/utils.py @@ -1,6 +1,5 @@ """authentik admin util views""" -from typing import Any, Optional -from urllib.parse import urlparse +from typing import Any from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin @@ -8,7 +7,7 @@ from django.contrib.postgres.search import SearchQuery, SearchVector from django.db.models import QuerySet from django.http import Http404 from django.http.request import HttpRequest -from django.views.generic import DeleteView, ListView, UpdateView +from django.views.generic import DeleteView, UpdateView from django.views.generic.list import MultipleObjectMixin from authentik.lib.utils.reflection import all_subclasses @@ -25,17 +24,6 @@ class DeleteMessageView(SuccessMessageMixin, DeleteView): return super().delete(request, *args, **kwargs) -class InheritanceListView(ListView): - """ListView for objects using InheritanceManager""" - - def get_context_data(self, **kwargs): - kwargs["types"] = {x.__name__: x for x in all_subclasses(self.model)} - return super().get_context_data(**kwargs) - - def get_queryset(self): - return super().get_queryset().select_subclasses() - - class SearchListMixin(MultipleObjectMixin): """Accept search query using `search` querystring parameter. Requires self.search_fields, a list of all fields to search. Can contain special lookups like __icontains""" @@ -98,23 +86,6 @@ class InheritanceUpdateView(UpdateView): ) -class BackSuccessUrlMixin: - """Checks if a relative URL has been given as ?back param, and redirect to it. Otherwise - default to self.success_url.""" - - request: HttpRequest - - success_url: Optional[str] - - def get_success_url(self) -> str: - """get_success_url from FormMixin""" - back_param = self.request.GET.get("back") - if back_param: - if not bool(urlparse(back_param).netloc): - return back_param - return str(self.success_url) - - class UserPaginateListMixin: """Get paginate_by value from user's attributes, defaulting to 15""" From 9d4c22c7061393fc295569c97ae68ebf0c50a06f Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 19 Feb 2021 19:33:58 +0100 Subject: [PATCH 30/34] web: show header while loading application info --- authentik/flows/api/stages.py | 2 +- authentik/flows/transfer/common.py | 1 + authentik/flows/transfer/importer.py | 19 +- swagger.yaml | 237 ++++++++++++------ .../pages/applications/ApplicationViewPage.ts | 9 +- web/src/pages/tokens/TokenListPage.ts | 7 +- 6 files changed, 186 insertions(+), 89 deletions(-) diff --git a/authentik/flows/api/stages.py b/authentik/flows/api/stages.py index 56ba52937..661a6e537 100644 --- a/authentik/flows/api/stages.py +++ b/authentik/flows/api/stages.py @@ -18,7 +18,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer): """Stage Serializer""" object_type = SerializerMethodField() - flow_set = FlowSerializer(many=True) + flow_set = FlowSerializer(many=True, required=False) def get_object_type(self, obj: Stage) -> str: """Get object type so that we know which API Endpoint to use to get the full object""" diff --git a/authentik/flows/transfer/common.py b/authentik/flows/transfer/common.py index 590b15e03..2b1173942 100644 --- a/authentik/flows/transfer/common.py +++ b/authentik/flows/transfer/common.py @@ -22,6 +22,7 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]: "verbose_name", "verbose_name_plural", "object_type", + "flow_set", ) for to_remove_name in to_remove: if to_remove_name in data: diff --git a/authentik/flows/transfer/importer.py b/authentik/flows/transfer/importer.py index 2839668a8..fd5ab82b6 100644 --- a/authentik/flows/transfer/importer.py +++ b/authentik/flows/transfer/importer.py @@ -65,14 +65,17 @@ class FlowImporter: return value for key, value in attrs.items(): - if isinstance(value, dict): - for idx, _inner_key in enumerate(value): - value[_inner_key] = updater(value[_inner_key]) - elif isinstance(value, list): - for idx, _inner_value in enumerate(value): - attrs[key][idx] = updater(_inner_value) - else: - attrs[key] = updater(value) + try: + if isinstance(value, dict): + for idx, _inner_key in enumerate(value): + value[_inner_key] = updater(value[_inner_key]) + elif isinstance(value, list): + for idx, _inner_value in enumerate(value): + attrs[key][idx] = updater(_inner_value) + else: + attrs[key] = updater(value) + except TypeError: + continue return attrs def __query_from_identifier(self, attrs: dict[str, Any]) -> Q: diff --git a/swagger.yaml b/swagger.yaml index 2cc4d87bd..78110582e 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -8570,82 +8570,6 @@ definitions: webhook_url: title: Webhook url type: string - Stage: - title: Stage obj - description: Stage Serializer - required: - - name - type: object - properties: - pk: - title: Stage uuid - type: string - format: uuid - readOnly: true - name: - title: Name - type: string - minLength: 1 - object_type: - title: Object type - type: string - readOnly: true - verbose_name: - title: Verbose name - type: string - readOnly: true - verbose_name_plural: - title: Verbose name plural - type: string - readOnly: true - FlowStageBinding: - description: FlowStageBinding Serializer - required: - - target - - stage - - order - type: object - properties: - pk: - title: Fsb uuid - type: string - format: uuid - readOnly: true - policybindingmodel_ptr_id: - title: Policybindingmodel ptr id - type: string - readOnly: true - target: - title: Target - type: string - format: uuid - stage: - title: Stage - type: string - format: uuid - stage_obj: - $ref: '#/definitions/Stage' - evaluate_on_plan: - title: Evaluate on plan - description: Evaluate policies during the Flow planning process. Disable this - for input-based policies. - type: boolean - re_evaluate_policies: - title: Re evaluate policies - description: Evaluate policies when the Stage is present to the user. - type: boolean - order: - title: Order - type: integer - maximum: 2147483647 - minimum: -2147483648 - policies: - type: array - items: - type: string - format: uuid - readOnly: true - uniqueItems: true Flow: description: Flow Serializer required: @@ -8718,6 +8642,87 @@ definitions: title: Cache count type: string readOnly: true + Stage: + title: Stage obj + description: Stage Serializer + required: + - name + type: object + properties: + pk: + title: Stage uuid + type: string + format: uuid + readOnly: true + name: + title: Name + type: string + minLength: 1 + object_type: + title: Object type + type: string + readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' + FlowStageBinding: + description: FlowStageBinding Serializer + required: + - target + - stage + - order + type: object + properties: + pk: + title: Fsb uuid + type: string + format: uuid + readOnly: true + policybindingmodel_ptr_id: + title: Policybindingmodel ptr id + type: string + readOnly: true + target: + title: Target + type: string + format: uuid + stage: + title: Stage + type: string + format: uuid + stage_obj: + $ref: '#/definitions/Stage' + evaluate_on_plan: + title: Evaluate on plan + description: Evaluate policies during the Flow planning process. Disable this + for input-based policies. + type: boolean + re_evaluate_policies: + title: Re evaluate policies + description: Evaluate policies when the Stage is present to the user. + type: boolean + order: + title: Order + type: integer + maximum: 2147483647 + minimum: -2147483648 + policies: + type: array + items: + type: string + format: uuid + readOnly: true + uniqueItems: true Cache: description: Generic cache stats for an object type: object @@ -10913,6 +10918,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' configure_flow: title: Configure flow description: Flow used by an authenticated user to configure this Stage. If @@ -10953,6 +10963,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' configure_flow: title: Configure flow description: Flow used by an authenticated user to configure this Stage. If @@ -10993,6 +11008,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' not_configured_action: title: Not configured action type: string @@ -11032,6 +11052,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' CaptchaStage: description: CaptchaStage Serializer required: @@ -11061,6 +11086,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' public_key: title: Public key description: Public key, acquired from https://www.google.com/recaptcha/intro/v3.html @@ -11098,6 +11128,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' mode: title: Mode type: string @@ -11137,6 +11172,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' EmailStage: description: EmailStage Serializer required: @@ -11164,6 +11204,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' use_global_settings: title: Use global settings description: When enabled, global Email connection settings will be used and @@ -11246,6 +11291,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' user_fields: description: '' type: array @@ -11312,6 +11362,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' continue_flow_without_invitation: title: Continue flow without invitation description: If this flag is set, this Stage will jump to the next Stage when @@ -11364,6 +11419,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' backends: description: '' type: array @@ -11464,6 +11524,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' fields: type: array items: @@ -11503,6 +11568,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' UserLoginStage: description: UserLoginStage Serializer required: @@ -11530,6 +11600,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' session_duration: title: Session duration description: 'Determines how long a session lasts. Default of 0 means that @@ -11563,6 +11638,11 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' UserWriteStage: description: UserWriteStage Serializer required: @@ -11590,3 +11670,8 @@ definitions: title: Verbose name plural type: string readOnly: true + flow_set: + description: '' + type: array + items: + $ref: '#/definitions/Flow' diff --git a/web/src/pages/applications/ApplicationViewPage.ts b/web/src/pages/applications/ApplicationViewPage.ts index 3b38bae6a..7ac812315 100644 --- a/web/src/pages/applications/ApplicationViewPage.ts +++ b/web/src/pages/applications/ApplicationViewPage.ts @@ -38,7 +38,14 @@ export class ApplicationViewPage extends LitElement { render(): TemplateResult { if (!this.application) { - return html``; + return html`
    +
    +

    + ${gettext("Loading...")} +

    +
    +
    + `; } return html`
    diff --git a/web/src/pages/tokens/TokenListPage.ts b/web/src/pages/tokens/TokenListPage.ts index dc4645bd5..488971c17 100644 --- a/web/src/pages/tokens/TokenListPage.ts +++ b/web/src/pages/tokens/TokenListPage.ts @@ -5,6 +5,7 @@ import { TablePage } from "../../elements/table/TablePage"; import "../../elements/buttons/ModalButton"; import "../../elements/buttons/Dropdown"; +import "../../elements/buttons/TokenCopyButton"; import { TableColumn } from "../../elements/table/Table"; import { Token } from "../../api/Tokens"; @@ -49,16 +50,16 @@ export class TokenListPage extends TablePage { html`${item.identifier}`, html`${item.user.username}`, html`${item.expiring ? "Yes" : "No"}`, - html`${item.expiring ? new Date(item.expires * 1000).toLocaleString() : '-'}`, + html`${item.expiring ? new Date(item.expires * 1000).toLocaleString() : "-"}`, html` - ${gettext('Delete')} + ${gettext("Delete")}
    - ${gettext('Copy Key')} + ${gettext("Copy Key")} `, ]; From 854d94056e072343c2d928dfeeb125885c3684a0 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 20 Feb 2021 00:09:53 +0100 Subject: [PATCH 31/34] web: migrate remaining list views to web --- .../administration/stage_invitation/list.html | 109 ----- .../administration/stage_prompt/list.html | 125 ----- authentik/admin/urls.py | 10 - authentik/admin/views/stages_invitations.py | 30 +- authentik/admin/views/stages_prompts.py | 38 +- authentik/api/v2/urls.py | 2 +- authentik/flows/transfer/common.py | 1 + authentik/stages/invitation/api.py | 5 + authentik/stages/prompt/api.py | 6 +- swagger.yaml | 454 ++++++++++++++---- web/src/api/Invitations.ts | 27 ++ web/src/api/Prompts.ts | 30 ++ web/src/elements/sidebar/Sidebar.ts | 2 +- web/src/interfaces/AdminInterface.ts | 4 +- web/src/pages/stages/InvitationListPage.ts | 72 +++ web/src/pages/stages/PromptListPage.ts | 84 ++++ web/src/routes.ts | 4 + 17 files changed, 606 insertions(+), 397 deletions(-) delete mode 100644 authentik/admin/templates/administration/stage_invitation/list.html delete mode 100644 authentik/admin/templates/administration/stage_prompt/list.html create mode 100644 web/src/api/Invitations.ts create mode 100644 web/src/api/Prompts.ts create mode 100644 web/src/pages/stages/InvitationListPage.ts create mode 100644 web/src/pages/stages/PromptListPage.ts diff --git a/authentik/admin/templates/administration/stage_invitation/list.html b/authentik/admin/templates/administration/stage_invitation/list.html deleted file mode 100644 index ec753169e..000000000 --- a/authentik/admin/templates/administration/stage_invitation/list.html +++ /dev/null @@ -1,109 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load authentik_utils %} - -{% block content %} -
    -
    -

    - - {% trans 'Invitations' %} -

    -

    {% trans "Create Invitation Links to enroll Users, and optionally force specific attributes of their account." %} -

    -
    -
    -
    -
    - {% if object_list %} -
    -
    - {% include 'partials/toolbar_search.html' %} -
    - - - {% trans 'Create' %} - -
    -
    - -
    - {% include 'partials/pagination.html' %} -
    -
    - - - - - - - - - - - {% for invitation in object_list %} - - - - - - - {% endfor %} - -
    {% trans 'ID' %}{% trans 'Created by' %}{% trans 'Expiry' %}
    - - {{ invitation.invite_uuid }} - - - - {{ invitation.created_by }} - - - - {{ invitation.expiry|default:"-" }} - - - - - {% trans 'Delete' %} - -
    -
    -
    -
    - {% include 'partials/pagination.html' %} -
    - {% else %} -
    -
    - {% include 'partials/toolbar_search.html' %} -
    -
    -
    -
    - -

    - {% trans 'No Invitations.' %} -

    -
    - {% if request.GET.search != "" %} - {% trans "Your search query doesn't match any invitations." %} - {% else %} - {% trans 'Currently no invitations exist. Click the button below to create one.' %} - {% endif %} -
    - - - {% trans 'Create' %} - -
    -
    -
    -
    - {% endif %} -
    -
    -{% endblock %} diff --git a/authentik/admin/templates/administration/stage_prompt/list.html b/authentik/admin/templates/administration/stage_prompt/list.html deleted file mode 100644 index 41b435e68..000000000 --- a/authentik/admin/templates/administration/stage_prompt/list.html +++ /dev/null @@ -1,125 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load authentik_utils %} - -{% block content %} -
    -
    -

    - - {% trans 'Prompts' %} -

    -

    {% trans "Single Prompts that can be used for Prompt Stages." %}

    -
    -
    -
    -
    - {% if object_list %} -
    -
    - {% include 'partials/toolbar_search.html' %} -
    - - - {% trans 'Create' %} - -
    -
    - -
    - {% include 'partials/pagination.html' %} -
    -
    - - - - - - - - - - - - - {% for prompt in object_list %} - - - - - - - - - {% endfor %} - -
    {% trans 'Field' %}{% trans 'Label' %}{% trans 'Type' %}{% trans 'Order' %}{% trans 'Flows' %}
    -
    -
    {{ prompt.field_key }}
    -
    -
    -
    - {{ prompt.label }} -
    -
    -
    - {{ prompt.type }} -
    -
    -
    - {{ prompt.order }} -
    -
    -
      - {% for flow in prompt.flow_set.all %} -
    • {{ flow.slug }}
    • - {% empty %} -
    • -
    • - {% endfor %} -
    -
    - - - {% trans 'Update' %} - -
    -
    - - - {% trans 'Delete' %} - -
    -
    -
    -
    - {% include 'partials/pagination.html' %} -
    - {% else %} -
    -
    - {% include 'partials/toolbar_search.html' %} -
    -
    -
    -
    - -

    - {% trans 'No Stage Prompts.' %} -

    -
    - {% if request.GET.search != "" %} - {% trans "Your search query doesn't match any stage prompts." %} - {% else %} - {% trans 'Currently no stage prompts exist. Click the button below to create one.' %} - {% endif %} -
    - {% trans 'Create' %} -
    -
    - {% endif %} -
    -
    -{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index 2ac5b256b..a8527e312 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -153,11 +153,6 @@ urlpatterns = [ name="stage-binding-delete", ), # Stage Prompts - path( - "stages_prompts/", - stages_prompts.PromptListView.as_view(), - name="stage-prompts", - ), path( "stages_prompts/create/", stages_prompts.PromptCreateView.as_view(), @@ -174,11 +169,6 @@ urlpatterns = [ name="stage-prompt-delete", ), # Stage Invitations - path( - "stages/invitations/", - stages_invitations.InvitationListView.as_view(), - name="stage-invitations", - ), path( "stages/invitations/create/", stages_invitations.InvitationCreateView.as_view(), diff --git a/authentik/admin/views/stages_invitations.py b/authentik/admin/views/stages_invitations.py index 8c6e0aa81..3e87247cc 100644 --- a/authentik/admin/views/stages_invitations.py +++ b/authentik/admin/views/stages_invitations.py @@ -5,37 +5,15 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.http import HttpResponseRedirect -from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from django.views.generic import ListView -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import ( - DeleteMessageView, - SearchListMixin, - UserPaginateListMixin, -) +from authentik.admin.views.utils import DeleteMessageView from authentik.lib.views import CreateAssignPermView from authentik.stages.invitation.forms import InvitationForm from authentik.stages.invitation.models import Invitation -class InvitationListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - ListView, -): - """Show list of all invitations""" - - model = Invitation - permission_required = "authentik_stages_invitation.view_invitation" - template_name = "administration/stage_invitation/list.html" - ordering = "-expires" - search_fields = ["created_by__username", "expires", "fixed_data"] - - class InvitationCreateView( SuccessMessageMixin, LoginRequiredMixin, @@ -49,7 +27,7 @@ class InvitationCreateView( permission_required = "authentik_stages_invitation.add_invitation" template_name = "generic/create.html" - success_url = reverse_lazy("authentik_admin:stage-invitations") + success_url = "/" success_message = _("Successfully created Invitation") def form_valid(self, form): @@ -68,5 +46,5 @@ class InvitationDeleteView( permission_required = "authentik_stages_invitation.delete_invitation" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:stage-invitations") + success_url = "/" success_message = _("Successfully deleted Invitation") diff --git a/authentik/admin/views/stages_prompts.py b/authentik/admin/views/stages_prompts.py index e1f182336..d61b04850 100644 --- a/authentik/admin/views/stages_prompts.py +++ b/authentik/admin/views/stages_prompts.py @@ -4,42 +4,16 @@ from django.contrib.auth.mixins import ( PermissionRequiredMixin as DjangoPermissionRequiredMixin, ) from django.contrib.messages.views import SuccessMessageMixin -from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from django.views.generic import ListView, UpdateView -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from django.views.generic import UpdateView +from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import ( - DeleteMessageView, - SearchListMixin, - UserPaginateListMixin, -) +from authentik.admin.views.utils import DeleteMessageView from authentik.lib.views import CreateAssignPermView from authentik.stages.prompt.forms import PromptAdminForm from authentik.stages.prompt.models import Prompt -class PromptListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - ListView, -): - """Show list of all prompts""" - - model = Prompt - permission_required = "authentik_stages_prompt.view_prompt" - ordering = "order" - template_name = "administration/stage_prompt/list.html" - search_fields = [ - "field_key", - "label", - "type", - "placeholder", - ] - - class PromptCreateView( SuccessMessageMixin, LoginRequiredMixin, @@ -53,7 +27,7 @@ class PromptCreateView( permission_required = "authentik_stages_prompt.add_prompt" template_name = "generic/create.html" - success_url = reverse_lazy("authentik_admin:stage-prompts") + success_url = "/" success_message = _("Successfully created Prompt") @@ -70,7 +44,7 @@ class PromptUpdateView( permission_required = "authentik_stages_prompt.change_prompt" template_name = "generic/update.html" - success_url = reverse_lazy("authentik_admin:stage-prompts") + success_url = "/" success_message = _("Successfully updated Prompt") @@ -81,5 +55,5 @@ class PromptDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessag permission_required = "authentik_stages_prompt.delete_prompt" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:stage-prompts") + success_url = "/" success_message = _("Successfully deleted Prompt") diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index 20323b7d4..a96cc1c6f 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -136,8 +136,8 @@ router.register("stages/captcha", CaptchaStageViewSet) router.register("stages/consent", ConsentStageViewSet) router.register("stages/email", EmailStageViewSet) router.register("stages/identification", IdentificationStageViewSet) -router.register("stages/invitation", InvitationStageViewSet) router.register("stages/invitation/invitations", InvitationViewSet) +router.register("stages/invitation/stages", InvitationStageViewSet) router.register("stages/password", PasswordStageViewSet) router.register("stages/prompt/prompts", PromptViewSet) router.register("stages/prompt/stages", PromptStageViewSet) diff --git a/authentik/flows/transfer/common.py b/authentik/flows/transfer/common.py index 2b1173942..c3cd629d0 100644 --- a/authentik/flows/transfer/common.py +++ b/authentik/flows/transfer/common.py @@ -23,6 +23,7 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]: "verbose_name_plural", "object_type", "flow_set", + "promptstage_set", ) for to_remove_name in to_remove: if to_remove_name in data: diff --git a/authentik/stages/invitation/api.py b/authentik/stages/invitation/api.py index a96363f38..8bc103c56 100644 --- a/authentik/stages/invitation/api.py +++ b/authentik/stages/invitation/api.py @@ -34,7 +34,9 @@ class InvitationSerializer(ModelSerializer): "pk", "expires", "fixed_data", + "created_by", ] + depth = 2 class InvitationViewSet(ModelViewSet): @@ -42,6 +44,9 @@ class InvitationViewSet(ModelViewSet): queryset = Invitation.objects.all() serializer_class = InvitationSerializer + order = ["-expires"] + search_fields = ["created_by__username", "expires"] + filterset_fields = ["created_by__username", "expires"] def perform_create(self, serializer: InvitationSerializer): serializer.instance.created_by = self.request.user diff --git a/authentik/stages/prompt/api.py b/authentik/stages/prompt/api.py index b6bdbf30b..a318ff646 100644 --- a/authentik/stages/prompt/api.py +++ b/authentik/stages/prompt/api.py @@ -31,6 +31,8 @@ class PromptStageViewSet(ModelViewSet): class PromptSerializer(ModelSerializer): """Prompt Serializer""" + promptstage_set = StageSerializer(many=True, required=False) + class Meta: model = Prompt @@ -42,11 +44,13 @@ class PromptSerializer(ModelSerializer): "required", "placeholder", "order", + "promptstage_set", ] class PromptViewSet(ModelViewSet): """Prompt Viewset""" - queryset = Prompt.objects.all() + queryset = Prompt.objects.all().prefetch_related("promptstage_set") serializer_class = PromptSerializer + filterset_fields = ["field_key", "label", "type", "placeholder"] diff --git a/swagger.yaml b/swagger.yaml index 78110582e..0aba5c0e2 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -6830,78 +6830,21 @@ paths: required: true type: string format: uuid - /stages/invitation/: - get: - operationId: stages_invitation_list - description: InvitationStage Viewset - parameters: - - name: ordering - in: query - description: Which field to use when ordering the results. - required: false - type: string - - name: search - in: query - description: A search term. - required: false - type: string - - name: page - in: query - description: A page number within the paginated result set. - required: false - type: integer - - name: page_size - in: query - description: Number of results to return per page. - required: false - type: integer - responses: - '200': - description: '' - schema: - required: - - count - - results - type: object - properties: - count: - type: integer - next: - type: string - format: uri - x-nullable: true - previous: - type: string - format: uri - x-nullable: true - results: - type: array - items: - $ref: '#/definitions/InvitationStage' - tags: - - stages - post: - operationId: stages_invitation_create - description: InvitationStage Viewset - parameters: - - name: data - in: body - required: true - schema: - $ref: '#/definitions/InvitationStage' - responses: - '201': - description: '' - schema: - $ref: '#/definitions/InvitationStage' - tags: - - stages - parameters: [] /stages/invitation/invitations/: get: operationId: stages_invitation_invitations_list description: Invitation Viewset parameters: + - name: created_by__username + in: query + description: '' + required: false + type: string + - name: expires + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -7024,9 +6967,76 @@ paths: required: true type: string format: uuid - /stages/invitation/{stage_uuid}/: + /stages/invitation/stages/: get: - operationId: stages_invitation_read + operationId: stages_invitation_stages_list + description: InvitationStage Viewset + parameters: + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/InvitationStage' + tags: + - stages + post: + operationId: stages_invitation_stages_create + description: InvitationStage Viewset + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/InvitationStage' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/InvitationStage' + tags: + - stages + parameters: [] + /stages/invitation/stages/{stage_uuid}/: + get: + operationId: stages_invitation_stages_read description: InvitationStage Viewset parameters: [] responses: @@ -7037,7 +7047,7 @@ paths: tags: - stages put: - operationId: stages_invitation_update + operationId: stages_invitation_stages_update description: InvitationStage Viewset parameters: - name: data @@ -7053,7 +7063,7 @@ paths: tags: - stages patch: - operationId: stages_invitation_partial_update + operationId: stages_invitation_stages_partial_update description: InvitationStage Viewset parameters: - name: data @@ -7069,7 +7079,7 @@ paths: tags: - stages delete: - operationId: stages_invitation_delete + operationId: stages_invitation_stages_delete description: InvitationStage Viewset parameters: [] responses: @@ -7216,6 +7226,26 @@ paths: operationId: stages_prompt_prompts_list description: Prompt Viewset parameters: + - name: field_key + in: query + description: '' + required: false + type: string + - name: label + in: query + description: '' + required: false + type: string + - name: type + in: query + description: '' + required: false + type: string + - name: placeholder + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -11335,6 +11365,263 @@ definitions: type: string format: uuid x-nullable: true + Invitation: + description: Invitation Serializer + type: object + properties: + pk: + title: Invite uuid + type: string + format: uuid + readOnly: true + expires: + title: Expires + type: string + format: date-time + x-nullable: true + fixed_data: + title: Fixed data + description: Optional fixed data to enforce on user enrollment. + type: object + created_by: + description: Custom User model to allow easier adding o f user-based settings + required: + - password + - username + - name + type: object + properties: + id: + title: ID + type: integer + readOnly: true + password: + title: Password + type: string + maxLength: 128 + minLength: 1 + last_login: + title: Last login + type: string + format: date-time + x-nullable: true + username: + title: Username + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + type: string + pattern: ^[\w.@+-]+$ + maxLength: 150 + minLength: 1 + first_name: + title: First name + type: string + maxLength: 150 + last_name: + title: Last name + type: string + maxLength: 150 + email: + title: Email address + type: string + format: email + maxLength: 254 + is_active: + title: Active + description: Designates whether this user should be treated as active. + Unselect this instead of deleting accounts. + type: boolean + date_joined: + title: Date joined + type: string + format: date-time + uuid: + title: Uuid + type: string + format: uuid + readOnly: true + name: + title: Name + description: User's display name. + type: string + minLength: 1 + password_change_date: + title: Password change date + type: string + format: date-time + readOnly: true + attributes: + title: Attributes + type: object + groups: + description: '' + type: array + items: + description: Groups are a generic way of categorizing users to apply + permissions, or some other label, to those users. A user can belong + to any number of groups. A user in a group automatically has all the + permissions granted to that group. For example, if the group 'Site + editors' has the permission can_edit_home_page, any user in that group + will have that permission. Beyond permissions, groups are a convenient + way to categorize users to apply some label, or extended functionality, + to them. For example, you could create a group 'Special users', and + you could write code that would do special things to those users -- + such as giving them access to a members-only portion of your site, + or sending them members-only email messages. + required: + - name + type: object + properties: + id: + title: ID + type: integer + readOnly: true + name: + title: Name + type: string + maxLength: 150 + minLength: 1 + permissions: + type: array + items: + type: integer + uniqueItems: true + readOnly: true + user_permissions: + description: '' + type: array + items: + description: "The permissions system provides a way to assign permissions\ + \ to specific users and groups of users. The permission system is\ + \ used by the Django admin site, but may also be useful in your own\ + \ code. The Django admin site uses permissions as follows: - The \"\ + add\" permission limits the user's ability to view the \"add\" form\ + \ and add an object. - The \"change\" permission limits a user's ability\ + \ to view the change list, view the \"change\" form and change an\ + \ object. - The \"delete\" permission limits the ability to delete\ + \ an object. - The \"view\" permission limits the ability to view\ + \ an object. Permissions are set globally per type of object, not\ + \ per specific object instance. It is possible to say \"Mary may change\ + \ news stories,\" but it's not currently possible to say \"Mary may\ + \ change news stories, but only the ones she created herself\" or\ + \ \"Mary may only change news stories that have a certain status or\ + \ publication date.\" The permissions listed above are automatically\ + \ created for each model." + required: + - name + - codename + - content_type + type: object + properties: + id: + title: ID + type: integer + readOnly: true + name: + title: Name + type: string + maxLength: 255 + minLength: 1 + codename: + title: Codename + type: string + maxLength: 100 + minLength: 1 + content_type: + title: Content type + type: integer + readOnly: true + sources: + description: '' + type: array + items: + description: Base Authentication source, i.e. an OAuth Provider, SAML + Remote or LDAP Server + required: + - name + - slug + type: object + properties: + pbm_uuid: + title: Pbm uuid + type: string + format: uuid + readOnly: true + name: + title: Name + description: Source's display Name. + type: string + minLength: 1 + slug: + title: Slug + description: Internal source name, used in URLs. + type: string + format: slug + pattern: ^[-a-zA-Z0-9_]+$ + maxLength: 50 + minLength: 1 + enabled: + title: Enabled + type: boolean + authentication_flow: + title: Authentication flow + description: Flow to use when authenticating existing users. + type: string + format: uuid + x-nullable: true + enrollment_flow: + title: Enrollment flow + description: Flow to use when enrolling new users. + type: string + format: uuid + x-nullable: true + policies: + type: array + items: + type: string + format: uuid + readOnly: true + uniqueItems: true + property_mappings: + type: array + items: + type: string + format: uuid + uniqueItems: true + readOnly: true + ak_groups: + description: '' + type: array + items: + description: Custom Group model which supports a basic hierarchy + required: + - name + - parent + type: object + properties: + group_uuid: + title: Group uuid + type: string + format: uuid + readOnly: true + name: + title: Name + type: string + maxLength: 80 + minLength: 1 + is_superuser: + title: Is superuser + description: Users added to this group will be superusers. + type: boolean + attributes: + title: Attributes + type: object + parent: + title: Parent + type: string + format: uuid + readOnly: true + readOnly: true InvitationStage: description: InvitationStage Serializer required: @@ -11373,24 +11660,6 @@ definitions: no Invitation is given. By default this Stage will cancel the Flow when no invitation is given. type: boolean - Invitation: - description: Invitation Serializer - type: object - properties: - pk: - title: Invite uuid - type: string - format: uuid - readOnly: true - expires: - title: Expires - type: string - format: date-time - x-nullable: true - fixed_data: - title: Fixed data - description: Optional fixed data to enforce on user enrollment. - type: object PasswordStage: description: PasswordStage Serializer required: @@ -11496,6 +11765,11 @@ definitions: type: integer maximum: 2147483647 minimum: -2147483648 + promptstage_set: + description: '' + type: array + items: + $ref: '#/definitions/Stage' PromptStage: description: PromptStage Serializer required: diff --git a/web/src/api/Invitations.ts b/web/src/api/Invitations.ts new file mode 100644 index 000000000..e28bd8c25 --- /dev/null +++ b/web/src/api/Invitations.ts @@ -0,0 +1,27 @@ +import { DefaultClient, QueryArguments, AKResponse } from "./Client"; +import { EventContext } from "./Events"; +import { User } from "./Users"; + +export class Invitation { + + pk: string; + expires: number; + fixed_date: EventContext; + created_by: User; + + constructor() { + throw Error(); + } + + static get(pk: string): Promise { + return DefaultClient.fetch(["stages", "invitation", "invitations", pk]); + } + + static list(filter?: QueryArguments): Promise> { + return DefaultClient.fetch>(["stages", "invitation", "invitations"], filter); + } + + static adminUrl(rest: string): string { + return `/administration/stages/invitations/${rest}`; + } +} diff --git a/web/src/api/Prompts.ts b/web/src/api/Prompts.ts new file mode 100644 index 000000000..b688f4812 --- /dev/null +++ b/web/src/api/Prompts.ts @@ -0,0 +1,30 @@ +import { DefaultClient, QueryArguments, AKResponse } from "./Client"; +import { Stage } from "./Flows"; + +export class Prompt { + + pk: string; + field_key: string; + label: string; + type: string; + required: boolean; + placeholder: string; + order: number; + promptstage_set: Stage[]; + + constructor() { + throw Error(); + } + + static get(pk: string): Promise { + return DefaultClient.fetch(["stages", "prompt", "prompts", pk]); + } + + static list(filter?: QueryArguments): Promise> { + return DefaultClient.fetch>(["stages", "prompt", "prompts"], filter); + } + + static adminUrl(rest: string): string { + return `/administration/stages/prompts/${rest}`; + } +} diff --git a/web/src/elements/sidebar/Sidebar.ts b/web/src/elements/sidebar/Sidebar.ts index da2203027..bf639b84e 100644 --- a/web/src/elements/sidebar/Sidebar.ts +++ b/web/src/elements/sidebar/Sidebar.ts @@ -29,7 +29,7 @@ export class SidebarItem { this.condition = async () => true; this.activeMatchers = []; if (this.path) { - this.activeMatchers.push(new RegExp(`^${this.path}`)); + this.activeMatchers.push(new RegExp(`^${this.path}$`)); } } diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index 704a212d3..b5c9f3325 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -41,8 +41,8 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ new SidebarItem("Flows").children( new SidebarItem("Flows", "/flow/flows").activeWhen(`^/flow/flows/(?${SLUG_REGEX})$`), new SidebarItem("Stages", "/flow/stages"), - new SidebarItem("Prompts", "/administration/stages_prompts/"), - new SidebarItem("Invitations", "/administration/stages/invitations/"), + new SidebarItem("Prompts", "/flow/stages/prompts"), + new SidebarItem("Invitations", "/flow/stages/invitations"), ).when((): Promise => { return User.me().then(u => u.is_superuser); }), diff --git a/web/src/pages/stages/InvitationListPage.ts b/web/src/pages/stages/InvitationListPage.ts new file mode 100644 index 000000000..e53cfd293 --- /dev/null +++ b/web/src/pages/stages/InvitationListPage.ts @@ -0,0 +1,72 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { AKResponse } from "../../api/Client"; +import { TablePage } from "../../elements/table/TablePage"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/SpinnerButton"; +import { TableColumn } from "../../elements/table/Table"; +import { Invitation } from "../../api/Invitations"; + +@customElement("ak-stage-invitation-list") +export class InvitationListPage extends TablePage { + searchEnabled(): boolean { + return true; + } + pageTitle(): string { + return gettext("Invitations"); + } + pageDescription(): string { + return gettext("Create Invitation Links to enroll Users, and optionally force specific attributes of their account."); + } + pageIcon(): string { + return gettext("pf-icon pf-icon-migration"); + } + + @property() + order = "expires"; + + apiEndpoint(page: number): Promise> { + return Invitation.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn("ID", "pk"), + new TableColumn("Created by", "created_by"), + new TableColumn("Expiry"), + new TableColumn(""), + ]; + } + + row(item: Invitation): TemplateResult[] { + return [ + html`${item.pk}`, + html`${item.created_by.username}`, + html`${new Date(item.expires * 1000).toLocaleString()}`, + html` + + + ${gettext("Delete")} + +
    +
    `, + ]; + } + + renderToolbar(): TemplateResult { + return html` + + + ${gettext("Create")} + +
    +
    + ${super.renderToolbar()} + `; + } +} diff --git a/web/src/pages/stages/PromptListPage.ts b/web/src/pages/stages/PromptListPage.ts new file mode 100644 index 000000000..37bc2c8f6 --- /dev/null +++ b/web/src/pages/stages/PromptListPage.ts @@ -0,0 +1,84 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { AKResponse } from "../../api/Client"; +import { TablePage } from "../../elements/table/TablePage"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/SpinnerButton"; +import { TableColumn } from "../../elements/table/Table"; +import { Prompt } from "../../api/Prompts"; + +@customElement("ak-stage-prompt-list") +export class PromptListPage extends TablePage { + searchEnabled(): boolean { + return true; + } + pageTitle(): string { + return gettext("Prompts"); + } + pageDescription(): string { + return gettext("Single Prompts that can be used for Prompt Stages."); + } + pageIcon(): string { + return gettext("pf-icon pf-icon-plugged"); + } + + @property() + order = "order"; + + apiEndpoint(page: number): Promise> { + return Prompt.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn("Field", "field_key"), + new TableColumn("Label", "label"), + new TableColumn("Type", "type"), + new TableColumn("Order", "order"), + new TableColumn("Stages"), + new TableColumn(""), + ]; + } + + row(item: Prompt): TemplateResult[] { + return [ + html`${item.field_key}`, + html`${item.label}`, + html`${item.type}`, + html`${item.order}`, + html`${item.promptstage_set.map((stage) => { + return html`
  • ${stage.name}
  • `; + })}`, + html` + + + ${gettext("Edit")} + +
    +
    + + + ${gettext("Delete")} + +
    +
    `, + ]; + } + + renderToolbar(): TemplateResult { + return html` + + + ${gettext("Create")} + +
    +
    + ${super.renderToolbar()} + `; + } +} diff --git a/web/src/routes.ts b/web/src/routes.ts index c33ce2f76..d30eb5f0f 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -22,6 +22,8 @@ import "./pages/providers/ProviderViewPage"; import "./pages/sources/SourcesListPage"; import "./pages/sources/SourceViewPage"; import "./pages/stages/StageListPage"; +import "./pages/stages/InvitationListPage"; +import "./pages/stages/PromptListPage"; import "./pages/system-tasks/SystemTaskListPage"; import "./pages/tokens/TokenListPage"; import "./pages/users/UserListPage"; @@ -49,6 +51,8 @@ export const ROUTES: Route[] = [ new Route(new RegExp("^/identity/groups$"), html``), new Route(new RegExp("^/identity/users$"), html``), new Route(new RegExp("^/core/tokens$"), html``), + new Route(new RegExp("^/flow/stages/invitations$"), html``), + new Route(new RegExp("^/flow/stages/prompts$"), html``), new Route(new RegExp("^/flow/stages$"), html``), new Route(new RegExp("^/flow/flows$"), html``), new Route(new RegExp(`^/flow/flows/(?${SLUG_REGEX})$`)).then((args) => { From 264c678eaa263f975ef9066da320150c7a88e36b Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 20 Feb 2021 00:20:01 +0100 Subject: [PATCH 32/34] web: migrate user token list to web --- authentik/core/templates/user/settings.html | 4 +- authentik/core/urls.py | 1 - authentik/core/views/user.py | 35 +---------- web/src/api/Tokens.ts | 4 ++ web/src/main.ts | 1 + web/src/pages/tokens/TokenListPage.ts | 2 +- web/src/pages/tokens/UserTokenList.ts | 65 +++++++++++++++++++++ 7 files changed, 75 insertions(+), 37 deletions(-) create mode 100644 web/src/pages/tokens/UserTokenList.ts diff --git a/authentik/core/templates/user/settings.html b/authentik/core/templates/user/settings.html index 047288c59..6305bc773 100644 --- a/authentik/core/templates/user/settings.html +++ b/authentik/core/templates/user/settings.html @@ -24,9 +24,7 @@
    - -
    -
    +
    {% user_stages as user_stages_loc %} {% for stage, stage_link in user_stages_loc.items %} diff --git a/authentik/core/urls.py b/authentik/core/urls.py index 9383191ab..f24a27e69 100644 --- a/authentik/core/urls.py +++ b/authentik/core/urls.py @@ -8,7 +8,6 @@ urlpatterns = [ # User views path("-/user/", user.UserSettingsView.as_view(), name="user-settings"), path("-/user/details/", user.UserDetailsView.as_view(), name="user-details"), - path("-/user/tokens/", user.TokenListView.as_view(), name="user-tokens"), path( "-/user/tokens/create/", user.TokenCreateView.as_view(), diff --git a/authentik/core/views/user.py b/authentik/core/views/user.py index 140c6e6da..13939ca4f 100644 --- a/authentik/core/views/user.py +++ b/authentik/core/views/user.py @@ -6,20 +6,15 @@ from django.contrib.auth.mixins import ( PermissionRequiredMixin as DjangoPermissionRequiredMixin, ) from django.contrib.messages.views import SuccessMessageMixin -from django.db.models.query import QuerySet from django.http.response import HttpResponse from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from django.views.generic import ListView, UpdateView +from django.views.generic import UpdateView from django.views.generic.base import TemplateView -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from guardian.mixins import PermissionRequiredMixin from guardian.shortcuts import get_objects_for_user -from authentik.admin.views.utils import ( - DeleteMessageView, - SearchListMixin, - UserPaginateListMixin, -) +from authentik.admin.views.utils import DeleteMessageView from authentik.core.forms.token import UserTokenForm from authentik.core.forms.users import UserDetailForm from authentik.core.models import Token, TokenIntents @@ -54,30 +49,6 @@ class UserDetailsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView): return kwargs -class TokenListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - ListView, -): - """Show list of all tokens""" - - model = Token - ordering = "expires" - permission_required = "authentik_core.view_token" - - template_name = "user/token_list.html" - search_fields = [ - "identifier", - "intent", - "description", - ] - - def get_queryset(self) -> QuerySet: - return super().get_queryset().filter(intent=TokenIntents.INTENT_API) - - class TokenCreateView( SuccessMessageMixin, LoginRequiredMixin, diff --git a/web/src/api/Tokens.ts b/web/src/api/Tokens.ts index e1c6ab1b0..9319efa7d 100644 --- a/web/src/api/Tokens.ts +++ b/web/src/api/Tokens.ts @@ -34,6 +34,10 @@ export class Token { return `/administration/tokens/${rest}`; } + static userUrl(rest: string): string { + return `/-/user/tokens/${rest}`; + } + static getKey(identifier: string): Promise { return DefaultClient.fetch<{ key: string }>(["core", "tokens", identifier, "view_key"]).then( (r) => r.key diff --git a/web/src/main.ts b/web/src/main.ts index 9922f7ff0..88b4eba36 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -29,6 +29,7 @@ import "./pages/admin-overview/AdminOverviewPage"; import "./pages/admin-overview/TopApplicationsTable"; import "./pages/applications/ApplicationListPage"; import "./pages/applications/ApplicationViewPage"; +import "./pages/tokens/UserTokenList"; import "./pages/LibraryPage"; import "./elements/stages/authenticator_webauthn/WebAuthnRegister"; diff --git a/web/src/pages/tokens/TokenListPage.ts b/web/src/pages/tokens/TokenListPage.ts index 488971c17..f96016d11 100644 --- a/web/src/pages/tokens/TokenListPage.ts +++ b/web/src/pages/tokens/TokenListPage.ts @@ -52,7 +52,7 @@ export class TokenListPage extends TablePage { html`${item.expiring ? "Yes" : "No"}`, html`${item.expiring ? new Date(item.expires * 1000).toLocaleString() : "-"}`, html` - + ${gettext("Delete")} diff --git a/web/src/pages/tokens/UserTokenList.ts b/web/src/pages/tokens/UserTokenList.ts new file mode 100644 index 000000000..3ad10a789 --- /dev/null +++ b/web/src/pages/tokens/UserTokenList.ts @@ -0,0 +1,65 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { AKResponse } from "../../api/Client"; +import { TablePage } from "../../elements/table/TablePage"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/Dropdown"; +import "../../elements/buttons/TokenCopyButton"; +import { Table, TableColumn } from "../../elements/table/Table"; +import { Token } from "../../api/Tokens"; + +@customElement("ak-token-user-list") +export class UserTokenList extends Table { + searchEnabled(): boolean { + return true; + } + + @property() + order = "expires"; + + apiEndpoint(page: number): Promise> { + return Token.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn("Identifier", "identifier"), + new TableColumn("User", "user"), + new TableColumn("Expires?", "expiring"), + new TableColumn("Expiry date", "expires"), + new TableColumn(""), + ]; + } + + row(item: Token): TemplateResult[] { + return [ + html`${item.identifier}`, + html`${item.user.username}`, + html`${item.expiring ? "Yes" : "No"}`, + html`${item.expiring ? new Date(item.expires * 1000).toLocaleString() : "-"}`, + html` + + + ${gettext("Edit")} + +
    +
    + + + ${gettext("Delete")} + +
    +
    + + ${gettext("Copy Key")} + + `, + ]; + } + +} From dde303f13ab46939d7df529f43543bd9923aabf3 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 20 Feb 2021 00:27:22 +0100 Subject: [PATCH 33/34] admin: remove dead code --- authentik/admin/views/utils.py | 35 ---------------------------------- 1 file changed, 35 deletions(-) diff --git a/authentik/admin/views/utils.py b/authentik/admin/views/utils.py index 8146a4607..f17b1b354 100644 --- a/authentik/admin/views/utils.py +++ b/authentik/admin/views/utils.py @@ -3,12 +3,8 @@ from typing import Any from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin -from django.contrib.postgres.search import SearchQuery, SearchVector -from django.db.models import QuerySet from django.http import Http404 -from django.http.request import HttpRequest from django.views.generic import DeleteView, UpdateView -from django.views.generic.list import MultipleObjectMixin from authentik.lib.utils.reflection import all_subclasses from authentik.lib.views import CreateAssignPermView @@ -24,26 +20,6 @@ class DeleteMessageView(SuccessMessageMixin, DeleteView): return super().delete(request, *args, **kwargs) -class SearchListMixin(MultipleObjectMixin): - """Accept search query using `search` querystring parameter. Requires self.search_fields, - a list of all fields to search. Can contain special lookups like __icontains""" - - search_fields: list[str] - - def get_queryset(self) -> QuerySet: - queryset = super().get_queryset() - if "search" in self.request.GET: - raw_query = self.request.GET["search"] - if raw_query == "": - # Empty query, don't search at all - return queryset - search = SearchQuery(raw_query, search_type="websearch") - return queryset.annotate(search=SearchVector(*self.search_fields)).filter( - search=search - ) - return queryset - - class InheritanceCreateView(CreateAssignPermView): """CreateView for objects using InheritanceManager""" @@ -84,14 +60,3 @@ class InheritanceUpdateView(UpdateView): .select_subclasses() .first() ) - - -class UserPaginateListMixin: - """Get paginate_by value from user's attributes, defaulting to 15""" - - request: HttpRequest - - # pylint: disable=unused-argument - def get_paginate_by(self, queryset: QuerySet) -> int: - """get_paginate_by Function of ListView""" - return self.request.user.attributes.get("paginate_by", 15) From 4f374c0c01c1986bde0f9cbd4c8da594bd8d5ecc Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 20 Feb 2021 00:27:32 +0100 Subject: [PATCH 34/34] web: add 404 page, don't auto-redirect to home --- web/src/elements/router/Router404.ts | 27 +++++++++++++++++++++++++ web/src/elements/router/RouterOutlet.ts | 10 ++++----- web/src/pages/tokens/UserTokenList.ts | 1 - 3 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 web/src/elements/router/Router404.ts diff --git a/web/src/elements/router/Router404.ts b/web/src/elements/router/Router404.ts new file mode 100644 index 000000000..6dc3fb3d0 --- /dev/null +++ b/web/src/elements/router/Router404.ts @@ -0,0 +1,27 @@ +import { gettext } from "django"; +import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; +import { COMMON_STYLES } from "../../common/styles"; + +@customElement("ak-router-404") +export class Router404 extends LitElement { + + @property() + url = ""; + + static get styles(): CSSResult[] { + return COMMON_STYLES; + } + + render(): TemplateResult { + return html`
    +
    + +

    ${gettext("Not found")}

    +
    + ${gettext(`The url '${this.url}' was not found.`)} +
    + ${gettext("Return home")} +
    +
    `; + } +} diff --git a/web/src/elements/router/RouterOutlet.ts b/web/src/elements/router/RouterOutlet.ts index 48f5d877e..513597975 100644 --- a/web/src/elements/router/RouterOutlet.ts +++ b/web/src/elements/router/RouterOutlet.ts @@ -10,6 +10,7 @@ import { ROUTES } from "../../routes"; import { RouteMatch } from "./RouteMatch"; import "../../pages/generic/SiteShell"; +import "./Router404"; @customElement("ak-router-outlet") export class RouterOutlet extends LitElement { @@ -67,12 +68,12 @@ export class RouterOutlet extends LitElement { } }); if (!matchedRoute) { - console.debug(`authentik/router: route "${activeUrl}" not defined, defaulting to shell`); + console.debug(`authentik/router: route "${activeUrl}" not defined`); const route = new Route( RegExp(""), - html` -
    -
    ` + html`
    + +
    ` ); matchedRoute = new RouteMatch(route); matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {}; @@ -82,7 +83,6 @@ export class RouterOutlet extends LitElement { } render(): TemplateResult | undefined { - // TODO: Render 404 when current Route is empty return this.current?.render(); } } diff --git a/web/src/pages/tokens/UserTokenList.ts b/web/src/pages/tokens/UserTokenList.ts index 3ad10a789..fba40547b 100644 --- a/web/src/pages/tokens/UserTokenList.ts +++ b/web/src/pages/tokens/UserTokenList.ts @@ -1,7 +1,6 @@ import { gettext } from "django"; import { customElement, html, property, TemplateResult } from "lit-element"; import { AKResponse } from "../../api/Client"; -import { TablePage } from "../../elements/table/TablePage"; import "../../elements/buttons/ModalButton"; import "../../elements/buttons/Dropdown";