From fe4791c21664207b9118e44f9bc55b3f528378d9 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 25 Mar 2021 00:02:35 +0100 Subject: [PATCH 01/60] web: initial implementation of new forms Signed-off-by: Jens Langhammer --- authentik/core/forms/users.py | 15 -- authentik/core/templates/user/details.html | 26 --- authentik/core/tests/test_views_user.py | 30 ---- authentik/core/urls.py | 1 - authentik/core/views/user.py | 28 ---- tests/e2e/test_flows_enroll.py | 4 +- tests/e2e/test_source_oauth.py | 6 +- tests/e2e/test_source_saml.py | 6 +- web/package-lock.json | 138 ++++++++++++++++ web/package.json | 2 + web/src/api/legacy.ts | 4 + web/src/authentik.css | 7 + web/src/main.ts | 1 - web/src/pages/users/UserDetailsPage.ts | 151 ++++++++++++++++++ web/src/pages/users/UserSettingsPage.ts | 30 +--- .../pages/{tokens => users}/UserTokenList.ts | 2 +- 16 files changed, 317 insertions(+), 134 deletions(-) delete mode 100644 authentik/core/forms/users.py delete mode 100644 authentik/core/templates/user/details.html delete mode 100644 authentik/core/tests/test_views_user.py create mode 100644 web/src/pages/users/UserDetailsPage.ts rename web/src/pages/{tokens => users}/UserTokenList.ts (99%) diff --git a/authentik/core/forms/users.py b/authentik/core/forms/users.py deleted file mode 100644 index 36b5e33c5..000000000 --- a/authentik/core/forms/users.py +++ /dev/null @@ -1,15 +0,0 @@ -"""authentik core user forms""" - -from django import forms - -from authentik.core.models import User - - -class UserDetailForm(forms.ModelForm): - """Update User Details""" - - class Meta: - - model = User - fields = ["username", "name", "email"] - widgets = {"name": forms.TextInput} diff --git a/authentik/core/templates/user/details.html b/authentik/core/templates/user/details.html deleted file mode 100644 index 0babe3137..000000000 --- a/authentik/core/templates/user/details.html +++ /dev/null @@ -1,26 +0,0 @@ -{% load i18n %} - -
-
- {% trans 'Update details' %} -
-
-
- {% include 'partials/form_horizontal.html' with form=form %} - {% block beneath_form %} - {% endblock %} -
-
-
- - {% if unenrollment_enabled %} - {% - trans "Delete account" %} - {% endif %} -
-
-
-
-
-
diff --git a/authentik/core/tests/test_views_user.py b/authentik/core/tests/test_views_user.py deleted file mode 100644 index bad0ab441..000000000 --- a/authentik/core/tests/test_views_user.py +++ /dev/null @@ -1,30 +0,0 @@ -"""authentik user view tests""" -import string -from random import SystemRandom - -from django.test import TestCase -from django.urls import reverse - -from authentik.core.models import User - - -class TestUserViews(TestCase): - """Test User Views""" - - def setUp(self): - super().setUp() - self.user = User.objects.create_user( - username="unittest user", - email="unittest@example.com", - password="".join( - SystemRandom().choice(string.ascii_uppercase + string.digits) - for _ in range(8) - ), - ) - self.client.force_login(self.user) - - def test_user_details(self): - """Test UserDetailsView""" - self.assertEqual( - self.client.get(reverse("authentik_core:user-details")).status_code, 200 - ) diff --git a/authentik/core/urls.py b/authentik/core/urls.py index ee03d748c..a8f2d91b6 100644 --- a/authentik/core/urls.py +++ b/authentik/core/urls.py @@ -14,7 +14,6 @@ urlpatterns = [ name="root-redirect", ), # User views - path("-/user/details/", user.UserDetailsView.as_view(), name="user-details"), path( "-/user/tokens/create/", user.TokenCreateView.as_view(), diff --git a/authentik/core/views/user.py b/authentik/core/views/user.py index 6d630e922..6c5a9d7d4 100644 --- a/authentik/core/views/user.py +++ b/authentik/core/views/user.py @@ -15,39 +15,11 @@ from guardian.mixins import PermissionRequiredMixin from guardian.shortcuts import get_objects_for_user from authentik.core.forms.token import UserTokenForm -from authentik.core.forms.users import UserDetailForm from authentik.core.models import Token, TokenIntents from authentik.flows.models import Flow, FlowDesignation from authentik.lib.views import CreateAssignPermView -class UserSettingsView(TemplateView): - """Multiple SiteShells for user details and all stages""" - - template_name = "user/settings.html" - - -class UserDetailsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView): - """Update User details""" - - template_name = "user/details.html" - form_class = UserDetailForm - - success_message = _("Successfully updated user.") - success_url = reverse_lazy("authentik_core:user-details") - - def get_object(self): - return self.request.user - - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - kwargs = super().get_context_data(**kwargs) - unenrollment_flow = Flow.with_policy( - self.request, designation=FlowDesignation.UNRENOLLMENT - ) - kwargs["unenrollment_enabled"] = bool(unenrollment_flow) - return kwargs - - class TokenCreateView( SuccessMessageMixin, LoginRequiredMixin, diff --git a/tests/e2e/test_flows_enroll.py b/tests/e2e/test_flows_enroll.py index abd4c976d..2a9bef063 100644 --- a/tests/e2e/test_flows_enroll.py +++ b/tests/e2e/test_flows_enroll.py @@ -98,7 +98,7 @@ class TestFlowsEnroll(SeleniumTestCase): wait = WebDriverWait(interface_admin, self.wait_timeout) wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar"))) - self.driver.get(self.if_admin_url("authentik_core:user-details")) + self.driver.get(self.if_admin_url("/user")) user = User.objects.get(username="foo") self.assertEqual(user.username, "foo") @@ -198,7 +198,7 @@ class TestFlowsEnroll(SeleniumTestCase): ) wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar"))) - self.driver.get(self.if_admin_url("authentik_core:user-details")) + self.driver.get(self.if_admin_url("/user")) self.assert_user(User.objects.get(username="foo")) diff --git a/tests/e2e/test_source_oauth.py b/tests/e2e/test_source_oauth.py index 85dbab6ab..9f1ca18af 100644 --- a/tests/e2e/test_source_oauth.py +++ b/tests/e2e/test_source_oauth.py @@ -160,7 +160,7 @@ class TestSourceOAuth2(SeleniumTestCase): # Wait until we've logged in self.wait_for_url(self.if_admin_url("/library")) - self.driver.get(self.url("authentik_core:user-details")) + self.driver.get(self.if_admin_url("/user")) self.assertEqual( self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo" @@ -255,7 +255,7 @@ class TestSourceOAuth2(SeleniumTestCase): # Wait until we've logged in self.wait_for_url(self.if_admin_url("/library")) - self.driver.get(self.url("authentik_core:user-details")) + self.driver.get(self.if_admin_url("/user")) self.assertEqual( self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo" @@ -359,7 +359,7 @@ class TestSourceOAuth1(SeleniumTestCase): sleep(2) # Wait until we've logged in self.wait_for_url(self.if_admin_url("/library")) - self.driver.get(self.url("authentik_core:user-details")) + self.driver.get(self.if_admin_url("/user")) self.assertEqual( self.driver.find_element(By.ID, "id_username").get_attribute("value"), diff --git a/tests/e2e/test_source_saml.py b/tests/e2e/test_source_saml.py index 413b3e69a..8ea4d926d 100644 --- a/tests/e2e/test_source_saml.py +++ b/tests/e2e/test_source_saml.py @@ -153,7 +153,7 @@ class TestSourceSAML(SeleniumTestCase): # Wait until we're logged in self.wait_for_url(self.if_admin_url("/library")) - self.driver.get(self.url("authentik_core:user-details")) + self.driver.get(self.if_admin_url("/user")) # Wait until we've loaded the user info page self.assertNotEqual( @@ -233,7 +233,7 @@ class TestSourceSAML(SeleniumTestCase): # Wait until we're logged in self.wait_for_url(self.if_admin_url("/library")) - self.driver.get(self.url("authentik_core:user-details")) + self.driver.get(self.if_admin_url("/user")) # Wait until we've loaded the user info page self.assertNotEqual( @@ -300,7 +300,7 @@ class TestSourceSAML(SeleniumTestCase): # Wait until we're logged in self.wait_for_url(self.if_admin_url("/library")) - self.driver.get(self.url("authentik_core:user-details")) + self.driver.get(self.if_admin_url("/user")) # Wait until we've loaded the user info page self.assertNotEqual( diff --git a/web/package-lock.json b/web/package-lock.json index e49a55ba7..4efea8bd9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -133,6 +133,139 @@ "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.90.5.tgz", "integrity": "sha512-Fe0C8UkzSjtacQ+fHXlFB/LHzrv/c2K4z479C6dboOgkGQE1FyB0wt1NBfxij0D++rhOy04OOYdE+Tr0JSlZKw==" }, + "@polymer/font-roboto": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@polymer/font-roboto/-/font-roboto-3.0.2.tgz", + "integrity": "sha512-tx5TauYSmzsIvmSqepUPDYbs4/Ejz2XbZ1IkD7JEGqkdNUJlh+9KU85G56Tfdk/xjEZ8zorFfN09OSwiMrIQWA==" + }, + "@polymer/iron-a11y-announcer": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.1.0.tgz", + "integrity": "sha512-lc5i4NKB8kSQHH0Hwu8WS3ym93m+J69OHJWSSBxwd17FI+h2wmgxDzeG9LI4ojMMck17/uc2pLe7g/UHt5/K/A==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-a11y-keys-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-a11y-keys-behavior/-/iron-a11y-keys-behavior-3.0.1.tgz", + "integrity": "sha512-lnrjKq3ysbBPT/74l0Fj0U9H9C35Tpw2C/tpJ8a+5g8Y3YJs1WSZYnEl1yOkw6sEyaxOq/1DkzH0+60gGu5/PQ==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-ajax": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-ajax/-/iron-ajax-3.0.1.tgz", + "integrity": "sha512-7+TPEAfWsRdhj1Y8UeF1759ktpVu+c3sG16rJiUC3wF9+woQ9xI1zUm2d59i7Yc3aDEJrR/Q8Y262KlOvyGVNg==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-autogrow-textarea": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.3.tgz", + "integrity": "sha512-5r0VkWrIlm0JIp5E5wlnvkw7slK72lFRZXncmrsLZF+6n1dg2rI8jt7xpFzSmUWrqpcyXwyKaGaDvUjl3j4JLA==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-behaviors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-behaviors/-/iron-behaviors-3.0.1.tgz", + "integrity": "sha512-IMEwcv1lhf1HSQxuyWOUIL0lOBwmeaoSTpgCJeP9IBYnuB1SPQngmfRuHKgK6/m9LQ9F9miC7p3HeQQUdKAE0w==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-flex-layout": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz", + "integrity": "sha512-7gB869czArF+HZcPTVSgvA7tXYFze9EKckvM95NB7SqYF+NnsQyhoXgKnpFwGyo95lUjUW9TFDLUwDXnCYFtkw==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-form": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-form/-/iron-form-3.0.1.tgz", + "integrity": "sha512-JwSQXHjYALsytCeBkXlY8aRwqgZuYIqzOk3iHuugb1RXOdZ7MZHyJhMDVBbscHjxqPKu/KaVzAjrcfwNNafzEA==", + "requires": { + "@polymer/iron-ajax": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-form-element-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-form-element-behavior/-/iron-form-element-behavior-3.0.1.tgz", + "integrity": "sha512-G/e2KXyL5AY7mMjmomHkGpgS0uAf4ovNpKhkuUTRnMuMJuf589bKqE85KN4ovE1Tzhv2hJoh/igyD6ekHiYU1A==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-input": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-input/-/iron-input-3.0.1.tgz", + "integrity": "sha512-WLx13kEcbH9GKbj9+pWR6pbJkA5kxn3796ynx6eQd2rueMyUfVTR3GzOvadBKsciUuIuzrxpBWZ2+3UcueVUQQ==", + "requires": { + "@polymer/iron-a11y-announcer": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-meta": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-meta/-/iron-meta-3.0.1.tgz", + "integrity": "sha512-pWguPugiLYmWFV9UWxLWzZ6gm4wBwQdDy4VULKwdHCqR7OP7u98h+XDdGZsSlDPv6qoryV/e3tGHlTIT0mbzJA==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-validatable-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-validatable-behavior/-/iron-validatable-behavior-3.0.1.tgz", + "integrity": "sha512-wwpYh6wOa4fNI+jH5EYKC7TVPYQ2OfgQqocWat7GsNWcsblKYhLYbwsvEY5nO0n2xKqNfZzDLrUom5INJN7msQ==", + "requires": { + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-input": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-input/-/paper-input-3.2.1.tgz", + "integrity": "sha512-6ghgwQKM6mS0hAQxQqj+tkeEY1VUBqAsrasAm8V5RpNcfSWQC/hhRFxU0beGuKTAhndzezDzWYP6Zz4b8fExGg==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-autogrow-textarea": "^3.0.0-pre.26", + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-input": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-styles": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-styles/-/paper-styles-3.0.1.tgz", + "integrity": "sha512-y6hmObLqlCx602TQiSBKHqjwkE7xmDiFkoxdYGaNjtv4xcysOTdVJsDR/R9UHwIaxJ7gHlthMSykir1nv78++g==", + "requires": { + "@polymer/font-roboto": "^3.0.1", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/polymer": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.4.1.tgz", + "integrity": "sha512-KPWnhDZibtqKrUz7enIPOiO4ZQoJNOuLwqrhV2MXzIt3VVnUVJVG5ORz4Z2sgO+UZ+/UZnPD0jqY+jmw/+a9mQ==", + "requires": { + "@webcomponents/shadycss": "^1.9.1" + } + }, "@rollup/plugin-typescript": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.2.0.tgz", @@ -490,6 +623,11 @@ "eslint-visitor-keys": "^2.0.0" } }, + "@webcomponents/shadycss": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.10.2.tgz", + "integrity": "sha512-9Iseu8bRtecb0klvv+WXZOVZatsRkbaH7M97Z+f+Pt909R4lDfgUODAnra23DOZTpeMTAkVpf4m/FZztN7Ox1A==" + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", diff --git a/web/package.json b/web/package.json index b5757b497..9921a4efa 100644 --- a/web/package.json +++ b/web/package.json @@ -12,6 +12,8 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.15.3", "@patternfly/patternfly": "^4.90.5", + "@polymer/iron-form": "^3.0.1", + "@polymer/paper-input": "^3.2.1", "@sentry/browser": "^6.2.3", "@sentry/tracing": "^6.2.3", "@types/chart.js": "^2.9.31", diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts index c8a520d71..9e2c7734f 100644 --- a/web/src/api/legacy.ts +++ b/web/src/api/legacy.ts @@ -105,6 +105,10 @@ export class AppURLManager { export class FlowURLManager { + static defaultUnenrollment(): string { + return "-/default/unenrollment/"; + } + static configure(stageUuid: string, rest: string): string { return `-/configure/${stageUuid}/${rest}`; } diff --git a/web/src/authentik.css b/web/src/authentik.css index 8d5da9ba4..3cf2133ce 100644 --- a/web/src/authentik.css +++ b/web/src/authentik.css @@ -88,6 +88,7 @@ body { @media (prefers-color-scheme: dark) { :root { + --ak-accent: #fd4b2d; --ak-dark-foreground: #fafafa; --ak-dark-foreground-darker: #bebebe; --ak-dark-foreground-link: #5a5cb9; @@ -100,6 +101,12 @@ body { --pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker); --pf-global--link--Color: var(--ak-dark-foreground-link); } + + paper-input { + /* --paper-input-container-color: var(--ak-accent); */ + --paper-input-container-input-color: var(--ak-dark-foreground); + } + /* Global page background colour */ .pf-c-page { --pf-c-page--BackgroundColor: var(--ak-dark-background); diff --git a/web/src/main.ts b/web/src/main.ts index f57e82208..12997f4e3 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -8,7 +8,6 @@ import "./elements/buttons/ModalButton"; import "./elements/buttons/SpinnerButton"; import "./elements/CodeMirror"; -import "./pages/tokens/UserTokenList"; import "./pages/generic/SiteShell"; import "./interfaces/AdminInterface"; import "./elements/messages/MessageContainer"; diff --git a/web/src/pages/users/UserDetailsPage.ts b/web/src/pages/users/UserDetailsPage.ts new file mode 100644 index 000000000..9ef3fb26c --- /dev/null +++ b/web/src/pages/users/UserDetailsPage.ts @@ -0,0 +1,151 @@ +import { gettext } from "django"; +import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; +import PFCard from "@patternfly/patternfly/components/Card/card.css"; +import AKGlobal from "../../authentik.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; +import { CoreApi, User } from "authentik-api"; +import { me } from "../../api/Users"; +import "../../elements/forms/FormElement"; +import "../../elements/EmptyState"; +import { FlowURLManager } from "../../api/legacy"; +import "@polymer/paper-input/paper-input"; +import "@polymer/iron-form/iron-form"; +import { DEFAULT_CONFIG } from "../../api/Config"; +import { PaperInputElement } from "@polymer/paper-input/paper-input"; +import { showMessage } from "../../elements/messages/MessageContainer"; + +export interface ErrorResponse { + [key: string]: string[]; +} + +@customElement("ak-form") +export class Form extends LitElement { + + @property() + successMessage = ""; + + @property() + send!: (data: Record) => Promise; + + submit(ev: Event): void { + ev.preventDefault(); + const ironForm = this.shadowRoot?.querySelector("iron-form"); + if (!ironForm) { + return; + } + const data = ironForm.serializeForm(); + this.send(data).then(() => { + showMessage({ + level_tag: "success", + message: this.successMessage + }); + }).catch((ex: Response) => { + if (ex.status > 399 && ex.status < 500) { + return ex.json(); + } + return ex; + }).then((errorMessage?: ErrorResponse) => { + if (!errorMessage) return; + const elements: PaperInputElement[] = ironForm._getSubmittableElements(); + elements.forEach((element) => { + const elementName = element.name; + if (!elementName) return; + if (elementName in errorMessage) { + element.errorMessage = errorMessage[elementName].join(", "); + element.invalid = true; + } + }); + }); + } + + render(): TemplateResult { + return html` { this.submit(ev); }}> + + `; + } + +} + +@customElement("ak-user-details") +export class UserDetailsPage extends LitElement { + + static get styles(): CSSResult[] { + return [PFBase, PFCard, PFForm, PFFormControl, PFButton, AKGlobal]; + } + + @property({attribute: false}) + user?: User; + + firstUpdated(): void { + me().then((user) => { + this.user = user.user; + }); + } + + render(): TemplateResult { + if (!this.user) { + return html` + `; + } + return html`
+
+ ${gettext("Update details")} +
+
+ { + return new CoreApi(DEFAULT_CONFIG).coreUsersUpdate({ + id: this.user?.pk || 0, + data: data as User + }); + }}> +
+ + +

${gettext("Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.")}

+ + +

${gettext("User's display name.")}

+ + + +
+
+
+ + + ${gettext("Delete account")} + +
+
+
+
+
+
+
`; + } + +} diff --git a/web/src/pages/users/UserSettingsPage.ts b/web/src/pages/users/UserSettingsPage.ts index eefdae207..75aa14574 100644 --- a/web/src/pages/users/UserSettingsPage.ts +++ b/web/src/pages/users/UserSettingsPage.ts @@ -18,8 +18,8 @@ import { DEFAULT_CONFIG } from "../../api/Config"; import { until } from "lit-html/directives/until"; import { ifDefined } from "lit-html/directives/if-defined"; import "../../elements/Tabs"; -import "../tokens/UserTokenList"; -import "../generic/SiteShell"; +import "./UserDetailsPage"; +import "./UserTokenList"; import "./settings/UserSettingsAuthenticatorTOTP"; import "./settings/UserSettingsAuthenticatorStatic"; import "./settings/UserSettingsAuthenticatorWebAuthnDevices"; @@ -48,13 +48,7 @@ export class UserSettingsPage extends LitElement { return html` `; default: - return html`
-
- -
-
-
-
`; + return html`

${gettext(`Error: unsupported stage settings: ${stage.component}`)}

`; } } @@ -64,13 +58,7 @@ export class UserSettingsPage extends LitElement { return html` `; default: - return html`
-
- -
-
-
-
`; + return html`

${gettext(`Error: unsupported source settings: ${source.component}`)}

`; } } @@ -88,16 +76,10 @@ export class UserSettingsPage extends LitElement {
-
-
- -
-
-
-
+
- +
${until(new StagesApi(DEFAULT_CONFIG).stagesAllUserSettings({}).then((stages) => { return stages.map((stage) => { diff --git a/web/src/pages/tokens/UserTokenList.ts b/web/src/pages/users/UserTokenList.ts similarity index 99% rename from web/src/pages/tokens/UserTokenList.ts rename to web/src/pages/users/UserTokenList.ts index 56cec2b0d..4c1d21c36 100644 --- a/web/src/pages/tokens/UserTokenList.ts +++ b/web/src/pages/users/UserTokenList.ts @@ -12,7 +12,7 @@ import { CoreApi, Token } from "authentik-api"; import { DEFAULT_CONFIG } from "../../api/Config"; import { AdminURLManager } from "../../api/legacy"; -@customElement("ak-token-user-list") +@customElement("ak-user-token-list") export class UserTokenList extends Table { searchEnabled(): boolean { return true; From cfcf7aa2ae27ef0be2f82925afc5be096cccc8e5 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 25 Mar 2021 09:52:19 +0100 Subject: [PATCH 02/60] web: separate forms into dedicated file Signed-off-by: Jens Langhammer --- web/src/elements/forms/Form.ts | 58 ++++++++++++++++++++++++++ web/src/pages/users/UserDetailsPage.ts | 56 +------------------------ 2 files changed, 59 insertions(+), 55 deletions(-) create mode 100644 web/src/elements/forms/Form.ts diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts new file mode 100644 index 000000000..823520a35 --- /dev/null +++ b/web/src/elements/forms/Form.ts @@ -0,0 +1,58 @@ +import "@polymer/paper-input/paper-input"; +import "@polymer/iron-form/iron-form"; +import { PaperInputElement } from "@polymer/paper-input/paper-input"; +import { showMessage } from "../../elements/messages/MessageContainer"; +import { customElement, html, LitElement, property, TemplateResult } from "lit-element"; + +export interface ErrorResponse { + [key: string]: string[]; +} + +@customElement("ak-form") +export class Form extends LitElement { + + @property() + successMessage = ""; + + @property() + send!: (data: Record) => Promise; + + submit(ev: Event): void { + ev.preventDefault(); + const ironForm = this.shadowRoot?.querySelector("iron-form"); + if (!ironForm) { + return; + } + const data = ironForm.serializeForm(); + this.send(data).then(() => { + showMessage({ + level_tag: "success", + message: this.successMessage + }); + }).catch((ex: Response) => { + if (ex.status > 399 && ex.status < 500) { + return ex.json(); + } + return ex; + }).then((errorMessage?: ErrorResponse) => { + if (!errorMessage) return; + const elements: PaperInputElement[] = ironForm._getSubmittableElements(); + elements.forEach((element) => { + const elementName = element.name; + if (!elementName) return; + if (elementName in errorMessage) { + element.errorMessage = errorMessage[elementName].join(", "); + element.invalid = true; + } + }); + }); + } + + render(): TemplateResult { + return html` { this.submit(ev); }}> + + `; + } + +} diff --git a/web/src/pages/users/UserDetailsPage.ts b/web/src/pages/users/UserDetailsPage.ts index 9ef3fb26c..6547dbccd 100644 --- a/web/src/pages/users/UserDetailsPage.ts +++ b/web/src/pages/users/UserDetailsPage.ts @@ -14,61 +14,7 @@ import { FlowURLManager } from "../../api/legacy"; import "@polymer/paper-input/paper-input"; import "@polymer/iron-form/iron-form"; import { DEFAULT_CONFIG } from "../../api/Config"; -import { PaperInputElement } from "@polymer/paper-input/paper-input"; -import { showMessage } from "../../elements/messages/MessageContainer"; - -export interface ErrorResponse { - [key: string]: string[]; -} - -@customElement("ak-form") -export class Form extends LitElement { - - @property() - successMessage = ""; - - @property() - send!: (data: Record) => Promise; - - submit(ev: Event): void { - ev.preventDefault(); - const ironForm = this.shadowRoot?.querySelector("iron-form"); - if (!ironForm) { - return; - } - const data = ironForm.serializeForm(); - this.send(data).then(() => { - showMessage({ - level_tag: "success", - message: this.successMessage - }); - }).catch((ex: Response) => { - if (ex.status > 399 && ex.status < 500) { - return ex.json(); - } - return ex; - }).then((errorMessage?: ErrorResponse) => { - if (!errorMessage) return; - const elements: PaperInputElement[] = ironForm._getSubmittableElements(); - elements.forEach((element) => { - const elementName = element.name; - if (!elementName) return; - if (elementName in errorMessage) { - element.errorMessage = errorMessage[elementName].join(", "); - element.invalid = true; - } - }); - }); - } - - render(): TemplateResult { - return html` { this.submit(ev); }}> - - `; - } - -} +import "../../elements/forms/Form"; @customElement("ak-user-details") export class UserDetailsPage extends LitElement { From 0b3980e5644d2144139c823ec6cc3eac07432096 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 25 Mar 2021 10:07:10 +0100 Subject: [PATCH 03/60] web: fix URLs for FlowURLManager Signed-off-by: Jens Langhammer --- web/src/api/legacy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts index 9e2c7734f..b24dfb43e 100644 --- a/web/src/api/legacy.ts +++ b/web/src/api/legacy.ts @@ -106,11 +106,11 @@ export class AppURLManager { export class FlowURLManager { static defaultUnenrollment(): string { - return "-/default/unenrollment/"; + return "/flows/-/default/unenrollment/"; } static configure(stageUuid: string, rest: string): string { - return `-/configure/${stageUuid}/${rest}`; + return `/flows/-/configure/${stageUuid}/${rest}`; } } From 469ba3a39188999bc0007261fc2b559110dffbab Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 25 Mar 2021 10:07:23 +0100 Subject: [PATCH 04/60] web/flows: fix WebAuthn register stage Signed-off-by: Jens Langhammer --- .../WebAuthnAuthenticatorRegisterStage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/flows/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts b/web/src/flows/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts index 84b10ca90..664086260 100644 --- a/web/src/flows/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts +++ b/web/src/flows/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts @@ -64,9 +64,9 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage { // post the transformed credential data to the server for validation // and storing the public key try { - const formData = new FormData(); - formData.set("response", JSON.stringify(newAssertionForServer)); - await this.host?.submit(formData); + await this.host?.submit({ + response: newAssertionForServer + }); } catch (err) { throw new Error(gettext(`Server validation of credential failed: ${err}`)); } From 2fade4e604560db618e3fa96e44ad460fcc4d3f8 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 25 Mar 2021 14:27:16 +0100 Subject: [PATCH 05/60] web/elements: add ModalForm Signed-off-by: Jens Langhammer --- web/src/elements/forms/Form.ts | 4 +- web/src/elements/forms/ModalForm.ts | 58 ++++++++++ web/src/pages/users/UserSettingsPage.ts | 2 +- .../pages/users/settings/BaseUserSettings.ts | 4 +- .../UserSettingsAuthenticatorWebAuthn.ts | 109 ++++++++++++++++++ ...serSettingsAuthenticatorWebAuthnDevices.ts | 71 ------------ 6 files changed, 173 insertions(+), 75 deletions(-) create mode 100644 web/src/elements/forms/ModalForm.ts create mode 100644 web/src/pages/users/settings/UserSettingsAuthenticatorWebAuthn.ts delete mode 100644 web/src/pages/users/settings/UserSettingsAuthenticatorWebAuthnDevices.ts diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index 823520a35..6d3b3ba05 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -17,14 +17,14 @@ export class Form extends LitElement { @property() send!: (data: Record) => Promise; - submit(ev: Event): void { + submit(ev: Event): Promise | undefined { ev.preventDefault(); const ironForm = this.shadowRoot?.querySelector("iron-form"); if (!ironForm) { return; } const data = ironForm.serializeForm(); - this.send(data).then(() => { + return this.send(data).then(() => { showMessage({ level_tag: "success", message: this.successMessage diff --git a/web/src/elements/forms/ModalForm.ts b/web/src/elements/forms/ModalForm.ts new file mode 100644 index 000000000..8538036ea --- /dev/null +++ b/web/src/elements/forms/ModalForm.ts @@ -0,0 +1,58 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { ModalButton } from "../buttons/ModalButton"; +import { Form } from "./Form"; + +@customElement("ak-forms-modal") +export class DeleteForm extends ModalButton { + + confirm(): void { + this.querySelectorAll
("ak-form").forEach(form => { + const formPromise = form.submit(new Event("submit")); + if (!formPromise) { + return; + } + formPromise.then(() => { + this.open = false; + }); + }); + } + + renderModalInner(): TemplateResult { + return html`
+
+

+ +

+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ { + this.confirm(); + }} + class="pf-m-primary"> + +   + { + this.open = false; + }} + class="pf-m-secondary"> + ${gettext("Cancel")} + +
`; + } + +} diff --git a/web/src/pages/users/UserSettingsPage.ts b/web/src/pages/users/UserSettingsPage.ts index 75aa14574..d2fb255b0 100644 --- a/web/src/pages/users/UserSettingsPage.ts +++ b/web/src/pages/users/UserSettingsPage.ts @@ -22,7 +22,7 @@ import "./UserDetailsPage"; import "./UserTokenList"; import "./settings/UserSettingsAuthenticatorTOTP"; import "./settings/UserSettingsAuthenticatorStatic"; -import "./settings/UserSettingsAuthenticatorWebAuthnDevices"; +import "./settings/UserSettingsAuthenticatorWebAuthn"; import "./settings/UserSettingsPassword"; import "./settings/SourceSettingsOAuth"; diff --git a/web/src/pages/users/settings/BaseUserSettings.ts b/web/src/pages/users/settings/BaseUserSettings.ts index a4c6d5b5f..5fbd68a99 100644 --- a/web/src/pages/users/settings/BaseUserSettings.ts +++ b/web/src/pages/users/settings/BaseUserSettings.ts @@ -3,6 +3,8 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import AKGlobal from "../../../authentik.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; export abstract class BaseUserSettings extends LitElement { @@ -10,7 +12,7 @@ export abstract class BaseUserSettings extends LitElement { objectId!: string; static get styles(): CSSResult[] { - return [PFBase, PFCard, PFButton, AKGlobal]; + return [PFBase, PFCard, PFButton, PFForm, PFFormControl, AKGlobal]; } } diff --git a/web/src/pages/users/settings/UserSettingsAuthenticatorWebAuthn.ts b/web/src/pages/users/settings/UserSettingsAuthenticatorWebAuthn.ts new file mode 100644 index 000000000..9a9425777 --- /dev/null +++ b/web/src/pages/users/settings/UserSettingsAuthenticatorWebAuthn.ts @@ -0,0 +1,109 @@ +import { CSSResult, customElement, html, TemplateResult } from "lit-element"; +import { gettext } from "django"; +import { AuthenticatorsApi, StagesApi, WebAuthnDevice } from "authentik-api"; +import { until } from "lit-html/directives/until"; +import { FlowURLManager } from "../../../api/legacy"; +import { DEFAULT_CONFIG } from "../../../api/Config"; +import { BaseUserSettings } from "./BaseUserSettings"; +import PFDataList from "@patternfly/patternfly/components/DataList/data-list.css"; +import "../../../elements/buttons/ModalButton"; +import "../../../elements/buttons/SpinnerButton"; +import "../../../elements/forms/DeleteForm"; +import "../../../elements/forms/Form"; +import "../../../elements/forms/ModalForm"; + +@customElement("ak-user-settings-authenticator-webauthn") +export class UserSettingsAuthenticatorWebAuthn extends BaseUserSettings { + + static get styles(): CSSResult[] { + return super.styles.concat(PFDataList); + } + + renderDelete(device: WebAuthnDevice): TemplateResult { + return html` { + return new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsWebauthnDelete({ + id: device.pk || 0 + }); + }}> + + `; + } + + renderUpdate(device: WebAuthnDevice): TemplateResult { + return html` + + ${gettext("Update")} + + + ${gettext("Update")} + + { + return new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsWebauthnUpdate({ + id: device.pk || 0, + data: data as WebAuthnDevice + }); + }}> + + + + + + +
`; + } + + render(): TemplateResult { + return html`
+
+ ${gettext("WebAuthn Devices")} +
+
+
    + ${until(new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsWebauthnList({}).then((devices) => { + return devices.results.map((device) => { + return html`
  • +
    +
    +
    ${device.name || "-"}
    +
    + ${gettext(`Created ${device.createdOn?.toLocaleString()}`)} +
    +
    + ${this.renderUpdate(device)} + ${this.renderDelete(device)} +
    +
    +
    +
  • `; + }); + }))} +
+
+ +
`; + } + +} diff --git a/web/src/pages/users/settings/UserSettingsAuthenticatorWebAuthnDevices.ts b/web/src/pages/users/settings/UserSettingsAuthenticatorWebAuthnDevices.ts deleted file mode 100644 index e4a306103..000000000 --- a/web/src/pages/users/settings/UserSettingsAuthenticatorWebAuthnDevices.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { customElement, html, TemplateResult } from "lit-element"; -import { gettext } from "django"; -import { AuthenticatorsApi, StagesApi } from "authentik-api"; -import { until } from "lit-html/directives/until"; -import { FlowURLManager, UserURLManager } from "../../../api/legacy"; -import { DEFAULT_CONFIG } from "../../../api/Config"; -import { BaseUserSettings } from "./BaseUserSettings"; -import "../../../elements/buttons/ModalButton"; -import "../../../elements/buttons/SpinnerButton"; -import "../../../elements/forms/DeleteForm"; - -@customElement("ak-user-settings-authenticator-webauthn") -export class UserSettingsAuthenticatorWebAuthnDevices extends BaseUserSettings { - - render(): TemplateResult { - return html`
-
- ${gettext("WebAuthn Devices")} -
-
-
    - ${until(new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsWebauthnList({}).then((devices) => { - return devices.results.map((device) => { - return html`
  • -
    -
    -
    ${device.name || "-"}
    -
    - ${gettext(`Created ${device.createdOn?.toLocaleString()}`)} -
    -
    - - - ${gettext("Update")} - -
    -
    - { - return new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsWebauthnDelete({ - id: device.pk || 0 - }); - }}> - - -
    -
    -
    -
  • `; - }); - }))} -
-
- -
`; - } - -} From 02212406c4e1666221ffdf6181a28e7d1cce7fca Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 25 Mar 2021 21:39:49 +0100 Subject: [PATCH 06/60] web: start migrating: groups form Signed-off-by: Jens Langhammer --- web/package-lock.json | 43 +++++++++++++++++++++++ web/package.json | 1 + web/src/authentik.css | 7 ++-- web/src/elements/forms/Form.ts | 35 ++++++++++++++----- web/src/elements/forms/ModalForm.ts | 4 +-- web/src/pages/groups/GroupForm.ts | 49 +++++++++++++++++++++++++++ web/src/pages/groups/GroupListPage.ts | 19 ++++++++--- web/src/pages/users/UserTokenList.ts | 2 +- 8 files changed, 141 insertions(+), 19 deletions(-) create mode 100644 web/src/pages/groups/GroupForm.ts diff --git a/web/package-lock.json b/web/package-lock.json index 4efea8bd9..6933a88ac 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -182,6 +182,16 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/iron-checked-element-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-checked-element-behavior/-/iron-checked-element-behavior-3.0.1.tgz", + "integrity": "sha512-aDr0cbCNVq49q+pOqa6CZutFh+wWpwPMLpEth9swx+GkAj+gCURhuQkaUYhIo5f2egDbEioR1aeHMnPlU9dQZA==", + "requires": { + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/iron-flex-layout": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz", @@ -234,6 +244,30 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/paper-behaviors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-behaviors/-/paper-behaviors-3.0.1.tgz", + "integrity": "sha512-6knhj69fPJejv8qR0kCSUY+Q0XjaUf0OSnkjRjmTJPAwSrRYtgqE+l6P1FfA+py1X/cUjgne9EF5rMZAKJIg1g==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-checked-element-behavior": "^3.0.0-pre.26", + "@polymer/paper-ripple": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-checkbox": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/paper-checkbox/-/paper-checkbox-3.1.0.tgz", + "integrity": "sha512-kXm6yDG1tT8if0XuJ2cc9NF+g8Ev4wG+rnf0a+Sx+O7J6fn1jcnBlYn72FlrfjVjDQZDBFmT6nynhD5PvFw8iQ==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-checked-element-behavior": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-ripple": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/paper-input": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@polymer/paper-input/-/paper-input-3.2.1.tgz", @@ -248,6 +282,15 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/paper-ripple": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@polymer/paper-ripple/-/paper-ripple-3.0.2.tgz", + "integrity": "sha512-DnLNvYIMsiayeICroYxx6Q6Hg1cUU8HN2sbutXazlemAlGqdq80qz3TIaVdbpbt/pvjcFGX2HtntMlPstCge8Q==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/paper-styles": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/paper-styles/-/paper-styles-3.0.1.tgz", diff --git a/web/package.json b/web/package.json index 9921a4efa..235074cdd 100644 --- a/web/package.json +++ b/web/package.json @@ -13,6 +13,7 @@ "@fortawesome/fontawesome-free": "^5.15.3", "@patternfly/patternfly": "^4.90.5", "@polymer/iron-form": "^3.0.1", + "@polymer/paper-checkbox": "^3.1.0", "@polymer/paper-input": "^3.2.1", "@sentry/browser": "^6.2.3", "@sentry/tracing": "^6.2.3", diff --git a/web/src/authentik.css b/web/src/authentik.css index 3cf2133ce..29559ee42 100644 --- a/web/src/authentik.css +++ b/web/src/authentik.css @@ -103,8 +103,11 @@ body { } paper-input { - /* --paper-input-container-color: var(--ak-accent); */ - --paper-input-container-input-color: var(--ak-dark-foreground); + /* --paper-input-container-input-color: var(--ak-dark-foreground); */ + --primary-text-color: var(--ak-dark-foreground); + } + paper-checkbox { + --primary-text-color: var(--ak-dark-foreground); } /* Global page background colour */ diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index 6d3b3ba05..81907318f 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -2,40 +2,52 @@ import "@polymer/paper-input/paper-input"; import "@polymer/iron-form/iron-form"; import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { showMessage } from "../../elements/messages/MessageContainer"; -import { customElement, html, LitElement, property, TemplateResult } from "lit-element"; +import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; +import PFCard from "@patternfly/patternfly/components/Card/card.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import AKGlobal from "../../authentik.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; export interface ErrorResponse { [key: string]: string[]; } @customElement("ak-form") -export class Form extends LitElement { +export class Form extends LitElement { @property() successMessage = ""; @property() - send!: (data: Record) => Promise; + send!: (data: T) => Promise; - submit(ev: Event): Promise | undefined { + static get styles(): CSSResult[] { + return [PFBase, PFCard, PFButton, PFForm, PFFormControl, AKGlobal]; + } + + submit(ev: Event): Promise | undefined { ev.preventDefault(); const ironForm = this.shadowRoot?.querySelector("iron-form"); if (!ironForm) { + console.warn("authentik/forms: failed to find iron-form"); return; } - const data = ironForm.serializeForm(); - return this.send(data).then(() => { + const data = ironForm.serializeForm() as T; + return this.send(data).then((r) => { showMessage({ level_tag: "success", message: this.successMessage }); + return r; }).catch((ex: Response) => { if (ex.status > 399 && ex.status < 500) { return ex.json(); } return ex; - }).then((errorMessage?: ErrorResponse) => { - if (!errorMessage) return; + }).then((errorMessage: ErrorResponse) => { + if (!errorMessage) return errorMessage; const elements: PaperInputElement[] = ironForm._getSubmittableElements(); elements.forEach((element) => { const elementName = element.name; @@ -45,13 +57,18 @@ export class Form extends LitElement { element.invalid = true; } }); + return errorMessage; }); } + renderForm(): TemplateResult { + return html``; + } + render(): TemplateResult { return html` { this.submit(ev); }}> - + ${this.renderForm()} `; } diff --git a/web/src/elements/forms/ModalForm.ts b/web/src/elements/forms/ModalForm.ts index 8538036ea..aeaa38416 100644 --- a/web/src/elements/forms/ModalForm.ts +++ b/web/src/elements/forms/ModalForm.ts @@ -4,10 +4,10 @@ import { ModalButton } from "../buttons/ModalButton"; import { Form } from "./Form"; @customElement("ak-forms-modal") -export class DeleteForm extends ModalButton { +export class ModalForm extends ModalButton { confirm(): void { - this.querySelectorAll
("ak-form").forEach(form => { + this.querySelectorAll>("ak-form").forEach(form => { const formPromise = form.submit(new Event("submit")); if (!formPromise) { return; diff --git a/web/src/pages/groups/GroupForm.ts b/web/src/pages/groups/GroupForm.ts new file mode 100644 index 000000000..6e2cb3678 --- /dev/null +++ b/web/src/pages/groups/GroupForm.ts @@ -0,0 +1,49 @@ +import { CoreApi, Group } from "authentik-api"; +import { gettext } from "django"; +import { customElement, property } from "lit-element"; +import { html, TemplateResult } from "lit-html"; +import { DEFAULT_CONFIG } from "../../api/Config"; +import { Form } from "../../elements/forms/Form"; +import { ifDefined } from "lit-html/directives/if-defined"; +import "@polymer/paper-input/paper-input"; +import "@polymer/iron-form/iron-form"; +import '@polymer/paper-checkbox/paper-checkbox.js'; + +@customElement("ak-group-form") +export class GroupForm extends Form { + + @property({attribute: false}) + group?: Group; + + send = (data: Group): Promise => { + if (this.group) { + return new CoreApi(DEFAULT_CONFIG).coreGroupsUpdate({ + groupUuid: this.group.pk || "", + data: data + }); + } else { + return new CoreApi(DEFAULT_CONFIG).coreGroupsCreate({ + data: data + }); + } + }; + + renderForm(): TemplateResult { + return html` + + + + ${gettext("Is superuser")} + +

${gettext("Users added to this group will be superusers.")}

+ `; + } + +} diff --git a/web/src/pages/groups/GroupListPage.ts b/web/src/pages/groups/GroupListPage.ts index fd4bf00ae..cdaee5797 100644 --- a/web/src/pages/groups/GroupListPage.ts +++ b/web/src/pages/groups/GroupListPage.ts @@ -11,6 +11,8 @@ import { PAGE_SIZE } from "../../constants"; import { CoreApi, Group } from "authentik-api"; import { DEFAULT_CONFIG } from "../../api/Config"; import { AdminURLManager } from "../../api/legacy"; +import "../../elements/forms/ModalForm"; +import "./GroupForm"; @customElement("ak-group-list") export class GroupListPage extends TablePage { @@ -56,12 +58,19 @@ export class GroupListPage extends TablePage { html`${item.users.keys.length}`, html`${item.isSuperuser ? "Yes" : "No"}`, html` - - + + + ${gettext("Update")} + + + ${gettext("Update Group")} + + + + + { renderToolbar(): TemplateResult { return html` - + ${gettext("Create")} From 72cca0473a59c77add7aa76159bee7e9a9dc3d77 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 25 Mar 2021 22:07:54 +0100 Subject: [PATCH 07/60] web/elements: throw error in form Signed-off-by: Jens Langhammer --- web/src/elements/forms/Form.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index 81907318f..a5cc50006 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -10,10 +10,18 @@ import AKGlobal from "../../authentik.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; -export interface ErrorResponse { +interface ErrorResponse { [key: string]: string[]; } +export class APIError extends Error { + + constructor(public response: ErrorResponse) { + super(); + } + +} + @customElement("ak-form") export class Form extends LitElement { @@ -27,7 +35,7 @@ export class Form extends LitElement { return [PFBase, PFCard, PFButton, PFForm, PFFormControl, AKGlobal]; } - submit(ev: Event): Promise | undefined { + submit(ev: Event): Promise | undefined { ev.preventDefault(); const ironForm = this.shadowRoot?.querySelector("iron-form"); if (!ironForm) { @@ -57,7 +65,7 @@ export class Form extends LitElement { element.invalid = true; } }); - return errorMessage; + throw new APIError(errorMessage); }); } From 2e58982419eb307b1c17fd93889889086efd4f34 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 25 Mar 2021 22:08:09 +0100 Subject: [PATCH 08/60] web/elements: fix detection of inner forms, catch errors and don't close modal Signed-off-by: Jens Langhammer --- web/src/elements/forms/ModalForm.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/src/elements/forms/ModalForm.ts b/web/src/elements/forms/ModalForm.ts index aeaa38416..194c7b21e 100644 --- a/web/src/elements/forms/ModalForm.ts +++ b/web/src/elements/forms/ModalForm.ts @@ -7,13 +7,15 @@ import { Form } from "./Form"; export class ModalForm extends ModalButton { confirm(): void { - this.querySelectorAll>("ak-form").forEach(form => { + this.querySelectorAll>("[slot=form]").forEach(form => { const formPromise = form.submit(new Event("submit")); if (!formPromise) { return; } - formPromise.then(() => { + formPromise.then((a) => { this.open = false; + }).catch((e) => { + console.log(e); }); }); } From 32fb90e056b8d2668cfebc0cbf181be033a921a5 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 27 Mar 2021 15:53:54 +0100 Subject: [PATCH 09/60] core: include full users in group API Signed-off-by: Jens Langhammer --- authentik/core/api/groups.py | 4 ++ swagger.yaml | 117 ++++++++++++++++++++++++----------- 2 files changed, 86 insertions(+), 35 deletions(-) diff --git a/authentik/core/api/groups.py b/authentik/core/api/groups.py index 2ca259fb4..801d5e618 100644 --- a/authentik/core/api/groups.py +++ b/authentik/core/api/groups.py @@ -2,16 +2,20 @@ from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet +from authentik.core.api.users import UserSerializer from authentik.core.models import Group class GroupSerializer(ModelSerializer): """Group Serializer""" + users = UserSerializer(many=True) + class Meta: model = Group fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"] + depth = 2 class GroupViewSet(ModelViewSet): diff --git a/swagger.yaml b/swagger.yaml index da9fcb68f..876f360ca 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -10934,42 +10934,7 @@ definitions: format: uuid readOnly: true uniqueItems: true - Group: - description: Group Serializer - required: - - name - - parent - - users - type: object - properties: - pk: - 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 - parent: - title: Parent - type: string - format: uuid - users: - type: array - items: - type: integer - uniqueItems: true - attributes: - title: Attributes - type: object User: - title: User description: User Serializer required: - username @@ -11020,6 +10985,88 @@ definitions: attributes: title: Attributes type: object + Group: + description: Group Serializer + required: + - name + - users + type: object + properties: + pk: + 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 + parent: + description: Custom Group model which supports a basic hierarchy + required: + - name + 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: + 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 + users: + description: '' + type: array + items: + $ref: '#/definitions/User' + attributes: + title: Attributes + type: object Token: description: Token Serializer required: From 2e6a264f98c0456cf462857406d833009b5e103d Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 27 Mar 2021 23:38:53 +0100 Subject: [PATCH 10/60] web: migrate group forms Signed-off-by: Jens Langhammer --- web/package-lock.json | 159 ++++++++++++++++++++++++++ web/package.json | 4 + web/src/elements/forms/Form.ts | 11 +- web/src/pages/groups/GroupForm.ts | 38 +++++- web/src/pages/groups/GroupListPage.ts | 2 +- 5 files changed, 209 insertions(+), 5 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 9c5b8d550..f125fff3b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -192,6 +192,25 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/iron-dropdown": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-dropdown/-/iron-dropdown-3.0.1.tgz", + "integrity": "sha512-22yLhepfcKjuQMfFmRHi/9MPKTqkzgRrmWWW0P5uqK++xle53k2QBO5VYUAYiCN3ZcxIi9lEhZ9YWGeQj2JBig==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/neon-animation": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-fit-behavior": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/iron-fit-behavior/-/iron-fit-behavior-3.1.0.tgz", + "integrity": "sha512-ABcgIYqrjhmUT8tiuolqeGttF/8pd3sEymUDrO1vXbZu4FWIvoLNndrMDFvs++AGd12Mjf5pYy84NJc6dB8Vig==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/iron-flex-layout": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz", @@ -217,6 +236,25 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/iron-icon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-icon/-/iron-icon-3.0.1.tgz", + "integrity": "sha512-QLPwirk+UPZNaLnMew9VludXA4CWUCenRewgEcGYwdzVgDPCDbXxy6vRJjmweZobMQv/oVLppT2JZtJFnPxX6g==", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-iconset-svg": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-iconset-svg/-/iron-iconset-svg-3.0.1.tgz", + "integrity": "sha512-XNwURbNHRw6u2fJe05O5fMYye6GSgDlDqCO+q6K1zAnKIrpgZwf2vTkBd5uCcZwsN0FyCB3mvNZx4jkh85dRDw==", + "requires": { + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/iron-input": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-input/-/iron-input-3.0.1.tgz", @@ -227,6 +265,28 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/iron-list": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/iron-list/-/iron-list-3.1.0.tgz", + "integrity": "sha512-Eiv6xd3h3oPmn8SXFntXVfC3ZnegH+KHAxiKLKcOASFSRY3mHnr2AdcnExUJ9ItoCMA5UzKaM/0U22eWzGERtA==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/iron-scroll-target-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-menu-behavior": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@polymer/iron-menu-behavior/-/iron-menu-behavior-3.0.2.tgz", + "integrity": "sha512-8dpASkFNBIkxAJWsFLWIO1M7tKM0+wKs3PqdeF/dDdBciwoaaFgC2K1XCZFZnbe2t9/nJgemXxVugGZAWpYCGg==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-selector": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/iron-meta": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-meta/-/iron-meta-3.0.1.tgz", @@ -235,6 +295,41 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/iron-overlay-behavior": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@polymer/iron-overlay-behavior/-/iron-overlay-behavior-3.0.3.tgz", + "integrity": "sha512-Q/Fp0+uOQQ145ebZ7T8Cxl4m1tUKYjyymkjcL2rXUm+aDQGb1wA1M1LYxUF5YBqd+9lipE0PTIiYwA2ZL/sznA==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-fit-behavior": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-resizable-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-resizable-behavior/-/iron-resizable-behavior-3.0.1.tgz", + "integrity": "sha512-FyHxRxFspVoRaeZSWpT3y0C9awomb4tXXolIJcZ7RvXhMP632V5lez+ch5G5SwK0LpnAPkg35eB0LPMFv+YMMQ==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-scroll-target-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-scroll-target-behavior/-/iron-scroll-target-behavior-3.0.1.tgz", + "integrity": "sha512-xg1WanG25BIkQE8rhuReqY9zx1K5M7F+YAIYpswEp5eyDIaZ1Y3vUmVeQ3KG+hiSugzI1M752azXN7kvyhOBcQ==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-selector": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-selector/-/iron-selector-3.0.1.tgz", + "integrity": "sha512-sBVk2uas6prW0glUe2xEJJYlvxmYzM40Au9OKbfDK2Qekou/fLKcBRyIYI39kuI8zWRaip8f3CI8qXcUHnKb1A==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/iron-validatable-behavior": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-validatable-behavior/-/iron-validatable-behavior-3.0.1.tgz", @@ -244,6 +339,16 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/neon-animation": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/neon-animation/-/neon-animation-3.0.1.tgz", + "integrity": "sha512-cDDc0llpVCe0ATbDS3clDthI54Bc8YwZIeTGGmBJleKOvbRTUC5+ssJmRL+VwVh+VM5FlnQlx760ppftY3uprg==", + "requires": { + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/iron-selector": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/paper-behaviors": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/paper-behaviors/-/paper-behaviors-3.0.1.tgz", @@ -268,6 +373,24 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/paper-dropdown-menu": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@polymer/paper-dropdown-menu/-/paper-dropdown-menu-3.2.0.tgz", + "integrity": "sha512-2ohwSHF+RLSK6kA0UkkMiMQF6EZcaEYWAA25kfisI6DWie7yozKrpQNsqvwfOEHU6DdDMIotrOtH1TM88YS8Zg==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/iron-iconset-svg": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-input": "^3.1.0", + "@polymer/paper-menu-button": "^3.1.0", + "@polymer/paper-ripple": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.3.1" + } + }, "@polymer/paper-input": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@polymer/paper-input/-/paper-input-3.2.1.tgz", @@ -282,6 +405,42 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/paper-item": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-item/-/paper-item-3.0.1.tgz", + "integrity": "sha512-KTk2N+GsYiI/HuubL3sxebZ6tteQbBOAp4QVLAnbjSPmwl+mJSDWk+omuadesU0bpkCwaWVs3fHuQsmXxy4pkw==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-listbox": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-listbox/-/paper-listbox-3.0.1.tgz", + "integrity": "sha512-vMLWFpYcggAPmEDBmK+96fFefacOG3GLB1EguTn8+ZkqI+328hNfw1MzHjH68rgCIIUtjmm+9qgB1Sy/MN0a/A==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-menu-behavior": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-menu-button": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/paper-menu-button/-/paper-menu-button-3.1.0.tgz", + "integrity": "sha512-q0G0/rvYD/FFmIBMGCQWjfXzRqwFw9+WHSYV4uOQzM1Ln8LMXSAd+2CENsbVwtMh6fmBePj15ZlU8SM2dt1WDQ==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-dropdown": "^3.0.0-pre.26", + "@polymer/iron-fit-behavior": "^3.1.0", + "@polymer/neon-animation": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/paper-ripple": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@polymer/paper-ripple/-/paper-ripple-3.0.2.tgz", diff --git a/web/package.json b/web/package.json index ec0f39b67..40d4c5203 100644 --- a/web/package.json +++ b/web/package.json @@ -14,7 +14,10 @@ "@patternfly/patternfly": "^4.90.5", "@polymer/iron-form": "^3.0.1", "@polymer/paper-checkbox": "^3.1.0", + "@polymer/paper-dropdown-menu": "^3.2.0", "@polymer/paper-input": "^3.2.1", + "@polymer/paper-item": "^3.0.1", + "@polymer/paper-listbox": "^3.0.1", "@sentry/browser": "^6.2.3", "@sentry/tracing": "^6.2.3", "@types/chart.js": "^2.9.31", @@ -28,6 +31,7 @@ "flowchart.js": "^1.15.0", "lit-element": "^2.4.0", "lit-html": "^1.3.0", + "multiselect-combo-box": "^2.5.0-beta.3", "rapidoc": "^8.4.9", "rollup": "^2.42.4", "rollup-plugin-copy": "^3.4.0", diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index a5cc50006..fd12a6785 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -2,13 +2,14 @@ import "@polymer/paper-input/paper-input"; import "@polymer/iron-form/iron-form"; import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { showMessage } from "../../elements/messages/MessageContainer"; -import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; +import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import AKGlobal from "../../authentik.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; +import { MessageLevel } from "../messages/Message"; interface ErrorResponse { [key: string]: string[]; @@ -32,7 +33,11 @@ export class Form extends LitElement { send!: (data: T) => Promise; static get styles(): CSSResult[] { - return [PFBase, PFCard, PFButton, PFForm, PFFormControl, AKGlobal]; + return [PFBase, PFCard, PFButton, PFForm, PFFormControl, AKGlobal, css` + select[multiple] { + height: 15em; + } + `]; } submit(ev: Event): Promise | undefined { @@ -45,7 +50,7 @@ export class Form extends LitElement { const data = ironForm.serializeForm() as T; return this.send(data).then((r) => { showMessage({ - level_tag: "success", + level: MessageLevel.success, message: this.successMessage }); return r; diff --git a/web/src/pages/groups/GroupForm.ts b/web/src/pages/groups/GroupForm.ts index 6e2cb3678..6ad02c63f 100644 --- a/web/src/pages/groups/GroupForm.ts +++ b/web/src/pages/groups/GroupForm.ts @@ -7,7 +7,11 @@ import { Form } from "../../elements/forms/Form"; import { ifDefined } from "lit-html/directives/if-defined"; import "@polymer/paper-input/paper-input"; import "@polymer/iron-form/iron-form"; -import '@polymer/paper-checkbox/paper-checkbox.js'; +import '@polymer/paper-checkbox/paper-checkbox'; +import '@polymer/paper-dropdown-menu/paper-dropdown-menu'; +import '@polymer/paper-listbox/paper-listbox'; +import '@polymer/paper-item/paper-item'; +import { until } from "lit-html/directives/until"; @customElement("ak-group-form") export class GroupForm extends Form { @@ -43,6 +47,38 @@ export class GroupForm extends Form { ${gettext("Is superuser")}

${gettext("Users added to this group will be superusers.")}

+ + + ${gettext("No parent")} + ${until(new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then(groups => { + return groups.results.map(group => { + return html`${group.name}`; + }) + }), html``)} + + +
+
+ +
+
+
+ +

${gettext("Hold control/command to select multiple items.")}

+
+
+
`; } diff --git a/web/src/pages/groups/GroupListPage.ts b/web/src/pages/groups/GroupListPage.ts index cdaee5797..c5183ba8e 100644 --- a/web/src/pages/groups/GroupListPage.ts +++ b/web/src/pages/groups/GroupListPage.ts @@ -55,7 +55,7 @@ export class GroupListPage extends TablePage { return [ html`${item.name}`, html`${item.parent || "-"}`, - html`${item.users.keys.length}`, + html`${item.users?.keys.length}`, html`${item.isSuperuser ? "Yes" : "No"}`, html` From 926636c331b206e85139179fd10607ff108ebc1a Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 27 Mar 2021 23:39:28 +0100 Subject: [PATCH 11/60] web: fix error handling in forms for non-server errors Signed-off-by: Jens Langhammer --- web/src/elements/forms/Form.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index fd12a6785..6a7c84282 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -59,8 +59,11 @@ export class Form extends LitElement { return ex.json(); } return ex; - }).then((errorMessage: ErrorResponse) => { + }).then((errorMessage: ErrorResponse | Error) => { if (!errorMessage) return errorMessage; + if (errorMessage instanceof Error) { + throw errorMessage; + } const elements: PaperInputElement[] = ironForm._getSubmittableElements(); elements.forEach((element) => { const elementName = element.name; From bd9c0efab73c51199aaa0c3ad3ccc590dd4e28e8 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 28 Mar 2021 22:03:48 +0200 Subject: [PATCH 12/60] core: use only user ids for group Signed-off-by: Jens Langhammer --- authentik/core/api/groups.py | 4 -- swagger.yaml | 117 +++++++++++------------------------ 2 files changed, 35 insertions(+), 86 deletions(-) diff --git a/authentik/core/api/groups.py b/authentik/core/api/groups.py index 801d5e618..2ca259fb4 100644 --- a/authentik/core/api/groups.py +++ b/authentik/core/api/groups.py @@ -2,20 +2,16 @@ from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet -from authentik.core.api.users import UserSerializer from authentik.core.models import Group class GroupSerializer(ModelSerializer): """Group Serializer""" - users = UserSerializer(many=True) - class Meta: model = Group fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"] - depth = 2 class GroupViewSet(ModelViewSet): diff --git a/swagger.yaml b/swagger.yaml index 876f360ca..da9fcb68f 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -10934,7 +10934,42 @@ definitions: format: uuid readOnly: true uniqueItems: true + Group: + description: Group Serializer + required: + - name + - parent + - users + type: object + properties: + pk: + 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 + parent: + title: Parent + type: string + format: uuid + users: + type: array + items: + type: integer + uniqueItems: true + attributes: + title: Attributes + type: object User: + title: User description: User Serializer required: - username @@ -10985,88 +11020,6 @@ definitions: attributes: title: Attributes type: object - Group: - description: Group Serializer - required: - - name - - users - type: object - properties: - pk: - 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 - parent: - description: Custom Group model which supports a basic hierarchy - required: - - name - 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: - 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 - users: - description: '' - type: array - items: - $ref: '#/definitions/User' - attributes: - title: Attributes - type: object Token: description: Token Serializer required: From 768d72ec24b702bdba0c1d7138fefb9b8225a8a9 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 28 Mar 2021 22:07:11 +0200 Subject: [PATCH 13/60] web/admin: implement groupform using webcomponents Signed-off-by: Jens Langhammer --- web/package-lock.json | 310 ---------------------------- web/package.json | 6 - web/src/elements/forms/Form.ts | 54 +++-- web/src/elements/forms/ModalForm.ts | 4 +- web/src/elements/forms/utils.ts | 16 ++ web/src/pages/groups/GroupForm.ts | 87 ++++---- 6 files changed, 91 insertions(+), 386 deletions(-) create mode 100644 web/src/elements/forms/utils.ts diff --git a/web/package-lock.json b/web/package-lock.json index f125fff3b..1026420cd 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -133,27 +133,6 @@ "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.90.5.tgz", "integrity": "sha512-Fe0C8UkzSjtacQ+fHXlFB/LHzrv/c2K4z479C6dboOgkGQE1FyB0wt1NBfxij0D++rhOy04OOYdE+Tr0JSlZKw==" }, - "@polymer/font-roboto": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@polymer/font-roboto/-/font-roboto-3.0.2.tgz", - "integrity": "sha512-tx5TauYSmzsIvmSqepUPDYbs4/Ejz2XbZ1IkD7JEGqkdNUJlh+9KU85G56Tfdk/xjEZ8zorFfN09OSwiMrIQWA==" - }, - "@polymer/iron-a11y-announcer": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.1.0.tgz", - "integrity": "sha512-lc5i4NKB8kSQHH0Hwu8WS3ym93m+J69OHJWSSBxwd17FI+h2wmgxDzeG9LI4ojMMck17/uc2pLe7g/UHt5/K/A==", - "requires": { - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-a11y-keys-behavior": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-a11y-keys-behavior/-/iron-a11y-keys-behavior-3.0.1.tgz", - "integrity": "sha512-lnrjKq3ysbBPT/74l0Fj0U9H9C35Tpw2C/tpJ8a+5g8Y3YJs1WSZYnEl1yOkw6sEyaxOq/1DkzH0+60gGu5/PQ==", - "requires": { - "@polymer/polymer": "^3.0.0" - } - }, "@polymer/iron-ajax": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-ajax/-/iron-ajax-3.0.1.tgz", @@ -162,63 +141,6 @@ "@polymer/polymer": "^3.0.0" } }, - "@polymer/iron-autogrow-textarea": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.3.tgz", - "integrity": "sha512-5r0VkWrIlm0JIp5E5wlnvkw7slK72lFRZXncmrsLZF+6n1dg2rI8jt7xpFzSmUWrqpcyXwyKaGaDvUjl3j4JLA==", - "requires": { - "@polymer/iron-behaviors": "^3.0.0-pre.26", - "@polymer/iron-flex-layout": "^3.0.0-pre.26", - "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-behaviors": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-behaviors/-/iron-behaviors-3.0.1.tgz", - "integrity": "sha512-IMEwcv1lhf1HSQxuyWOUIL0lOBwmeaoSTpgCJeP9IBYnuB1SPQngmfRuHKgK6/m9LQ9F9miC7p3HeQQUdKAE0w==", - "requires": { - "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-checked-element-behavior": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-checked-element-behavior/-/iron-checked-element-behavior-3.0.1.tgz", - "integrity": "sha512-aDr0cbCNVq49q+pOqa6CZutFh+wWpwPMLpEth9swx+GkAj+gCURhuQkaUYhIo5f2egDbEioR1aeHMnPlU9dQZA==", - "requires": { - "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", - "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-dropdown": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-dropdown/-/iron-dropdown-3.0.1.tgz", - "integrity": "sha512-22yLhepfcKjuQMfFmRHi/9MPKTqkzgRrmWWW0P5uqK++xle53k2QBO5VYUAYiCN3ZcxIi9lEhZ9YWGeQj2JBig==", - "requires": { - "@polymer/iron-behaviors": "^3.0.0-pre.26", - "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", - "@polymer/neon-animation": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-fit-behavior": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@polymer/iron-fit-behavior/-/iron-fit-behavior-3.1.0.tgz", - "integrity": "sha512-ABcgIYqrjhmUT8tiuolqeGttF/8pd3sEymUDrO1vXbZu4FWIvoLNndrMDFvs++AGd12Mjf5pYy84NJc6dB8Vig==", - "requires": { - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-flex-layout": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz", - "integrity": "sha512-7gB869czArF+HZcPTVSgvA7tXYFze9EKckvM95NB7SqYF+NnsQyhoXgKnpFwGyo95lUjUW9TFDLUwDXnCYFtkw==", - "requires": { - "@polymer/polymer": "^3.0.0" - } - }, "@polymer/iron-form": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-form/-/iron-form-3.0.1.tgz", @@ -228,238 +150,6 @@ "@polymer/polymer": "^3.0.0" } }, - "@polymer/iron-form-element-behavior": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-form-element-behavior/-/iron-form-element-behavior-3.0.1.tgz", - "integrity": "sha512-G/e2KXyL5AY7mMjmomHkGpgS0uAf4ovNpKhkuUTRnMuMJuf589bKqE85KN4ovE1Tzhv2hJoh/igyD6ekHiYU1A==", - "requires": { - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-icon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-icon/-/iron-icon-3.0.1.tgz", - "integrity": "sha512-QLPwirk+UPZNaLnMew9VludXA4CWUCenRewgEcGYwdzVgDPCDbXxy6vRJjmweZobMQv/oVLppT2JZtJFnPxX6g==", - "requires": { - "@polymer/iron-flex-layout": "^3.0.0-pre.26", - "@polymer/iron-meta": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-iconset-svg": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-iconset-svg/-/iron-iconset-svg-3.0.1.tgz", - "integrity": "sha512-XNwURbNHRw6u2fJe05O5fMYye6GSgDlDqCO+q6K1zAnKIrpgZwf2vTkBd5uCcZwsN0FyCB3mvNZx4jkh85dRDw==", - "requires": { - "@polymer/iron-meta": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-input": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-input/-/iron-input-3.0.1.tgz", - "integrity": "sha512-WLx13kEcbH9GKbj9+pWR6pbJkA5kxn3796ynx6eQd2rueMyUfVTR3GzOvadBKsciUuIuzrxpBWZ2+3UcueVUQQ==", - "requires": { - "@polymer/iron-a11y-announcer": "^3.0.0-pre.26", - "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-list": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@polymer/iron-list/-/iron-list-3.1.0.tgz", - "integrity": "sha512-Eiv6xd3h3oPmn8SXFntXVfC3ZnegH+KHAxiKLKcOASFSRY3mHnr2AdcnExUJ9ItoCMA5UzKaM/0U22eWzGERtA==", - "requires": { - "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", - "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", - "@polymer/iron-scroll-target-behavior": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-menu-behavior": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@polymer/iron-menu-behavior/-/iron-menu-behavior-3.0.2.tgz", - "integrity": "sha512-8dpASkFNBIkxAJWsFLWIO1M7tKM0+wKs3PqdeF/dDdBciwoaaFgC2K1XCZFZnbe2t9/nJgemXxVugGZAWpYCGg==", - "requires": { - "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", - "@polymer/iron-flex-layout": "^3.0.0-pre.26", - "@polymer/iron-selector": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-meta": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-meta/-/iron-meta-3.0.1.tgz", - "integrity": "sha512-pWguPugiLYmWFV9UWxLWzZ6gm4wBwQdDy4VULKwdHCqR7OP7u98h+XDdGZsSlDPv6qoryV/e3tGHlTIT0mbzJA==", - "requires": { - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-overlay-behavior": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@polymer/iron-overlay-behavior/-/iron-overlay-behavior-3.0.3.tgz", - "integrity": "sha512-Q/Fp0+uOQQ145ebZ7T8Cxl4m1tUKYjyymkjcL2rXUm+aDQGb1wA1M1LYxUF5YBqd+9lipE0PTIiYwA2ZL/sznA==", - "requires": { - "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", - "@polymer/iron-fit-behavior": "^3.0.0-pre.26", - "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-resizable-behavior": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-resizable-behavior/-/iron-resizable-behavior-3.0.1.tgz", - "integrity": "sha512-FyHxRxFspVoRaeZSWpT3y0C9awomb4tXXolIJcZ7RvXhMP632V5lez+ch5G5SwK0LpnAPkg35eB0LPMFv+YMMQ==", - "requires": { - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-scroll-target-behavior": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-scroll-target-behavior/-/iron-scroll-target-behavior-3.0.1.tgz", - "integrity": "sha512-xg1WanG25BIkQE8rhuReqY9zx1K5M7F+YAIYpswEp5eyDIaZ1Y3vUmVeQ3KG+hiSugzI1M752azXN7kvyhOBcQ==", - "requires": { - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-selector": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-selector/-/iron-selector-3.0.1.tgz", - "integrity": "sha512-sBVk2uas6prW0glUe2xEJJYlvxmYzM40Au9OKbfDK2Qekou/fLKcBRyIYI39kuI8zWRaip8f3CI8qXcUHnKb1A==", - "requires": { - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/iron-validatable-behavior": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-validatable-behavior/-/iron-validatable-behavior-3.0.1.tgz", - "integrity": "sha512-wwpYh6wOa4fNI+jH5EYKC7TVPYQ2OfgQqocWat7GsNWcsblKYhLYbwsvEY5nO0n2xKqNfZzDLrUom5INJN7msQ==", - "requires": { - "@polymer/iron-meta": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/neon-animation": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/neon-animation/-/neon-animation-3.0.1.tgz", - "integrity": "sha512-cDDc0llpVCe0ATbDS3clDthI54Bc8YwZIeTGGmBJleKOvbRTUC5+ssJmRL+VwVh+VM5FlnQlx760ppftY3uprg==", - "requires": { - "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", - "@polymer/iron-selector": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/paper-behaviors": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/paper-behaviors/-/paper-behaviors-3.0.1.tgz", - "integrity": "sha512-6knhj69fPJejv8qR0kCSUY+Q0XjaUf0OSnkjRjmTJPAwSrRYtgqE+l6P1FfA+py1X/cUjgne9EF5rMZAKJIg1g==", - "requires": { - "@polymer/iron-behaviors": "^3.0.0-pre.26", - "@polymer/iron-checked-element-behavior": "^3.0.0-pre.26", - "@polymer/paper-ripple": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/paper-checkbox": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@polymer/paper-checkbox/-/paper-checkbox-3.1.0.tgz", - "integrity": "sha512-kXm6yDG1tT8if0XuJ2cc9NF+g8Ev4wG+rnf0a+Sx+O7J6fn1jcnBlYn72FlrfjVjDQZDBFmT6nynhD5PvFw8iQ==", - "requires": { - "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", - "@polymer/iron-checked-element-behavior": "^3.0.0-pre.26", - "@polymer/paper-behaviors": "^3.0.0-pre.27", - "@polymer/paper-ripple": "^3.0.0-pre.26", - "@polymer/paper-styles": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/paper-dropdown-menu": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@polymer/paper-dropdown-menu/-/paper-dropdown-menu-3.2.0.tgz", - "integrity": "sha512-2ohwSHF+RLSK6kA0UkkMiMQF6EZcaEYWAA25kfisI6DWie7yozKrpQNsqvwfOEHU6DdDMIotrOtH1TM88YS8Zg==", - "requires": { - "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", - "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", - "@polymer/iron-icon": "^3.0.0-pre.26", - "@polymer/iron-iconset-svg": "^3.0.0-pre.26", - "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", - "@polymer/paper-behaviors": "^3.0.0-pre.27", - "@polymer/paper-input": "^3.1.0", - "@polymer/paper-menu-button": "^3.1.0", - "@polymer/paper-ripple": "^3.0.0-pre.26", - "@polymer/paper-styles": "^3.0.0-pre.26", - "@polymer/polymer": "^3.3.1" - } - }, - "@polymer/paper-input": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@polymer/paper-input/-/paper-input-3.2.1.tgz", - "integrity": "sha512-6ghgwQKM6mS0hAQxQqj+tkeEY1VUBqAsrasAm8V5RpNcfSWQC/hhRFxU0beGuKTAhndzezDzWYP6Zz4b8fExGg==", - "requires": { - "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", - "@polymer/iron-autogrow-textarea": "^3.0.0-pre.26", - "@polymer/iron-behaviors": "^3.0.0-pre.26", - "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", - "@polymer/iron-input": "^3.0.0-pre.26", - "@polymer/paper-styles": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/paper-item": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/paper-item/-/paper-item-3.0.1.tgz", - "integrity": "sha512-KTk2N+GsYiI/HuubL3sxebZ6tteQbBOAp4QVLAnbjSPmwl+mJSDWk+omuadesU0bpkCwaWVs3fHuQsmXxy4pkw==", - "requires": { - "@polymer/iron-behaviors": "^3.0.0-pre.26", - "@polymer/iron-flex-layout": "^3.0.0-pre.26", - "@polymer/paper-styles": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/paper-listbox": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/paper-listbox/-/paper-listbox-3.0.1.tgz", - "integrity": "sha512-vMLWFpYcggAPmEDBmK+96fFefacOG3GLB1EguTn8+ZkqI+328hNfw1MzHjH68rgCIIUtjmm+9qgB1Sy/MN0a/A==", - "requires": { - "@polymer/iron-behaviors": "^3.0.0-pre.26", - "@polymer/iron-menu-behavior": "^3.0.0-pre.26", - "@polymer/paper-styles": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/paper-menu-button": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@polymer/paper-menu-button/-/paper-menu-button-3.1.0.tgz", - "integrity": "sha512-q0G0/rvYD/FFmIBMGCQWjfXzRqwFw9+WHSYV4uOQzM1Ln8LMXSAd+2CENsbVwtMh6fmBePj15ZlU8SM2dt1WDQ==", - "requires": { - "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", - "@polymer/iron-behaviors": "^3.0.0-pre.26", - "@polymer/iron-dropdown": "^3.0.0-pre.26", - "@polymer/iron-fit-behavior": "^3.1.0", - "@polymer/neon-animation": "^3.0.0-pre.26", - "@polymer/paper-styles": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/paper-ripple": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@polymer/paper-ripple/-/paper-ripple-3.0.2.tgz", - "integrity": "sha512-DnLNvYIMsiayeICroYxx6Q6Hg1cUU8HN2sbutXazlemAlGqdq80qz3TIaVdbpbt/pvjcFGX2HtntMlPstCge8Q==", - "requires": { - "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/paper-styles": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/paper-styles/-/paper-styles-3.0.1.tgz", - "integrity": "sha512-y6hmObLqlCx602TQiSBKHqjwkE7xmDiFkoxdYGaNjtv4xcysOTdVJsDR/R9UHwIaxJ7gHlthMSykir1nv78++g==", - "requires": { - "@polymer/font-roboto": "^3.0.1", - "@polymer/iron-flex-layout": "^3.0.0-pre.26", - "@polymer/polymer": "^3.0.0" - } - }, "@polymer/polymer": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.4.1.tgz", diff --git a/web/package.json b/web/package.json index 40d4c5203..c1c65e5c5 100644 --- a/web/package.json +++ b/web/package.json @@ -13,11 +13,6 @@ "@fortawesome/fontawesome-free": "^5.15.3", "@patternfly/patternfly": "^4.90.5", "@polymer/iron-form": "^3.0.1", - "@polymer/paper-checkbox": "^3.1.0", - "@polymer/paper-dropdown-menu": "^3.2.0", - "@polymer/paper-input": "^3.2.1", - "@polymer/paper-item": "^3.0.1", - "@polymer/paper-listbox": "^3.0.1", "@sentry/browser": "^6.2.3", "@sentry/tracing": "^6.2.3", "@types/chart.js": "^2.9.31", @@ -31,7 +26,6 @@ "flowchart.js": "^1.15.0", "lit-element": "^2.4.0", "lit-html": "^1.3.0", - "multiselect-combo-box": "^2.5.0-beta.3", "rapidoc": "^8.4.9", "rollup": "^2.42.4", "rollup-plugin-copy": "^3.4.0", diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index 6a7c84282..5028579f9 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -10,6 +10,7 @@ import AKGlobal from "../../authentik.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; import { MessageLevel } from "../messages/Message"; +import { IronFormElement } from "@polymer/iron-form/iron-form"; interface ErrorResponse { [key: string]: string[]; @@ -40,6 +41,23 @@ export class Form extends LitElement { `]; } + serializeForm(form: IronFormElement): T { + const elements = form._getSubmittableElements(); + const json: { [key: string]: unknown } = {}; + for (let i = 0; i < elements.length; i++) { + const element = elements[i] as HTMLInputElement; + const values = form._serializeElementValues(element); + if (element.tagName.toLowerCase() === "select" && "multiple" in element.attributes) { + json[element.name] = values; + } else { + for (let v = 0; v < values.length; v++) { + form._addSerializedElement(json, element.name, values[v]); + } + } + } + return json as unknown as T; + } + submit(ev: Event): Promise | undefined { ev.preventDefault(); const ironForm = this.shadowRoot?.querySelector("iron-form"); @@ -47,7 +65,7 @@ export class Form extends LitElement { console.warn("authentik/forms: failed to find iron-form"); return; } - const data = ironForm.serializeForm() as T; + const data = this.serializeForm(ironForm); return this.send(data).then((r) => { showMessage({ level: MessageLevel.success, @@ -56,24 +74,24 @@ export class Form extends LitElement { return r; }).catch((ex: Response) => { if (ex.status > 399 && ex.status < 500) { - return ex.json(); + return ex.json().then((errorMessage: ErrorResponse) => { + if (!errorMessage) return errorMessage; + if (errorMessage instanceof Error) { + throw errorMessage; + } + const elements: PaperInputElement[] = ironForm._getSubmittableElements(); + elements.forEach((element) => { + const elementName = element.name; + if (!elementName) return; + if (elementName in errorMessage) { + element.errorMessage = errorMessage[elementName].join(", "); + element.invalid = true; + } + }); + throw new APIError(errorMessage); + }); } - return ex; - }).then((errorMessage: ErrorResponse | Error) => { - if (!errorMessage) return errorMessage; - if (errorMessage instanceof Error) { - throw errorMessage; - } - const elements: PaperInputElement[] = ironForm._getSubmittableElements(); - elements.forEach((element) => { - const elementName = element.name; - if (!elementName) return; - if (elementName in errorMessage) { - element.errorMessage = errorMessage[elementName].join(", "); - element.invalid = true; - } - }); - throw new APIError(errorMessage); + throw ex; }); } diff --git a/web/src/elements/forms/ModalForm.ts b/web/src/elements/forms/ModalForm.ts index 194c7b21e..488e4ebcf 100644 --- a/web/src/elements/forms/ModalForm.ts +++ b/web/src/elements/forms/ModalForm.ts @@ -1,5 +1,5 @@ import { gettext } from "django"; -import { customElement, html, property, TemplateResult } from "lit-element"; +import { customElement, html, TemplateResult } from "lit-element"; import { ModalButton } from "../buttons/ModalButton"; import { Form } from "./Form"; @@ -12,7 +12,7 @@ export class ModalForm extends ModalButton { if (!formPromise) { return; } - formPromise.then((a) => { + formPromise.then(() => { this.open = false; }).catch((e) => { console.log(e); diff --git a/web/src/elements/forms/utils.ts b/web/src/elements/forms/utils.ts new file mode 100644 index 000000000..79ff0dc68 --- /dev/null +++ b/web/src/elements/forms/utils.ts @@ -0,0 +1,16 @@ +import { TemplateResult, html } from "lit-html"; + +export function formGroup(label: string, body: TemplateResult): TemplateResult { + return html`
+
+ +
+
+
+ ${body} +
+
+
`; +} diff --git a/web/src/pages/groups/GroupForm.ts b/web/src/pages/groups/GroupForm.ts index 6ad02c63f..996f016d4 100644 --- a/web/src/pages/groups/GroupForm.ts +++ b/web/src/pages/groups/GroupForm.ts @@ -4,14 +4,9 @@ import { customElement, property } from "lit-element"; import { html, TemplateResult } from "lit-html"; import { DEFAULT_CONFIG } from "../../api/Config"; import { Form } from "../../elements/forms/Form"; -import { ifDefined } from "lit-html/directives/if-defined"; -import "@polymer/paper-input/paper-input"; -import "@polymer/iron-form/iron-form"; -import '@polymer/paper-checkbox/paper-checkbox'; -import '@polymer/paper-dropdown-menu/paper-dropdown-menu'; -import '@polymer/paper-listbox/paper-listbox'; -import '@polymer/paper-item/paper-item'; import { until } from "lit-html/directives/until"; +import { formGroup } from "../../elements/forms/utils"; +import { ifDefined } from "lit-html/directives/if-defined"; @customElement("ak-group-form") export class GroupForm extends Form { @@ -19,6 +14,8 @@ export class GroupForm extends Form { @property({attribute: false}) group?: Group; + successMessage = gettext("Successfully updated group"); + send = (data: Group): Promise => { if (this.group) { return new CoreApi(DEFAULT_CONFIG).coreGroupsUpdate({ @@ -34,51 +31,41 @@ export class GroupForm extends Form { renderForm(): TemplateResult { return html`
- - - - ${gettext("Is superuser")} - -

${gettext("Users added to this group will be superusers.")}

- - - ${gettext("No parent")} - ${until(new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then(groups => { - return groups.results.map(group => { - return html`${group.name}`; - }) - }), html``)} - - -
-
-
+

${gettext("Users added to this group will be superusers.")}

+ `)} + ${formGroup(gettext("Parent"), html` + + `)} + ${formGroup(gettext("Members"), html` + +

${gettext("Hold control/command to select multiple items.")}

+ `)} `; } From fbc33815a328a74529bbcfb2fcd87e0180cf5690 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 28 Mar 2021 22:33:27 +0200 Subject: [PATCH 14/60] core: fix user view imports Signed-off-by: Jens Langhammer --- authentik/core/views/user.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/authentik/core/views/user.py b/authentik/core/views/user.py index 6c5a9d7d4..968547434 100644 --- a/authentik/core/views/user.py +++ b/authentik/core/views/user.py @@ -1,22 +1,17 @@ """authentik core user views""" -from typing import Any - from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import ( PermissionRequiredMixin as DjangoPermissionRequiredMixin, ) from django.contrib.messages.views import SuccessMessageMixin from django.http.response import HttpResponse -from django.urls import reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import UpdateView -from django.views.generic.base import TemplateView from guardian.mixins import PermissionRequiredMixin from guardian.shortcuts import get_objects_for_user from authentik.core.forms.token import UserTokenForm from authentik.core.models import Token, TokenIntents -from authentik.flows.models import Flow, FlowDesignation from authentik.lib.views import CreateAssignPermView From 76e571ea0aa78731259deb087637ce657cf6e2c6 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 29 Mar 2021 10:09:43 +0200 Subject: [PATCH 15/60] web: use custom-element as wrapper Signed-off-by: Jens Langhammer --- web/package-lock.json | 108 ++++++++++++++++++ web/package.json | 1 + .../elements/forms/HorizontalFormElement.ts | 62 ++++++++++ web/src/elements/forms/utils.ts | 16 --- web/src/pages/groups/GroupForm.ts | 72 ++++++------ 5 files changed, 207 insertions(+), 52 deletions(-) create mode 100644 web/src/elements/forms/HorizontalFormElement.ts delete mode 100644 web/src/elements/forms/utils.ts diff --git a/web/package-lock.json b/web/package-lock.json index 90af1fe52..be1559cb1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -133,6 +133,27 @@ "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.90.5.tgz", "integrity": "sha512-Fe0C8UkzSjtacQ+fHXlFB/LHzrv/c2K4z479C6dboOgkGQE1FyB0wt1NBfxij0D++rhOy04OOYdE+Tr0JSlZKw==" }, + "@polymer/font-roboto": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@polymer/font-roboto/-/font-roboto-3.0.2.tgz", + "integrity": "sha512-tx5TauYSmzsIvmSqepUPDYbs4/Ejz2XbZ1IkD7JEGqkdNUJlh+9KU85G56Tfdk/xjEZ8zorFfN09OSwiMrIQWA==" + }, + "@polymer/iron-a11y-announcer": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.1.0.tgz", + "integrity": "sha512-lc5i4NKB8kSQHH0Hwu8WS3ym93m+J69OHJWSSBxwd17FI+h2wmgxDzeG9LI4ojMMck17/uc2pLe7g/UHt5/K/A==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-a11y-keys-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-a11y-keys-behavior/-/iron-a11y-keys-behavior-3.0.1.tgz", + "integrity": "sha512-lnrjKq3ysbBPT/74l0Fj0U9H9C35Tpw2C/tpJ8a+5g8Y3YJs1WSZYnEl1yOkw6sEyaxOq/1DkzH0+60gGu5/PQ==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/iron-ajax": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-ajax/-/iron-ajax-3.0.1.tgz", @@ -141,6 +162,34 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/iron-autogrow-textarea": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.3.tgz", + "integrity": "sha512-5r0VkWrIlm0JIp5E5wlnvkw7slK72lFRZXncmrsLZF+6n1dg2rI8jt7xpFzSmUWrqpcyXwyKaGaDvUjl3j4JLA==", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-behaviors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-behaviors/-/iron-behaviors-3.0.1.tgz", + "integrity": "sha512-IMEwcv1lhf1HSQxuyWOUIL0lOBwmeaoSTpgCJeP9IBYnuB1SPQngmfRuHKgK6/m9LQ9F9miC7p3HeQQUdKAE0w==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-flex-layout": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz", + "integrity": "sha512-7gB869czArF+HZcPTVSgvA7tXYFze9EKckvM95NB7SqYF+NnsQyhoXgKnpFwGyo95lUjUW9TFDLUwDXnCYFtkw==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/iron-form": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-form/-/iron-form-3.0.1.tgz", @@ -150,6 +199,65 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/iron-form-element-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-form-element-behavior/-/iron-form-element-behavior-3.0.1.tgz", + "integrity": "sha512-G/e2KXyL5AY7mMjmomHkGpgS0uAf4ovNpKhkuUTRnMuMJuf589bKqE85KN4ovE1Tzhv2hJoh/igyD6ekHiYU1A==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-input": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-input/-/iron-input-3.0.1.tgz", + "integrity": "sha512-WLx13kEcbH9GKbj9+pWR6pbJkA5kxn3796ynx6eQd2rueMyUfVTR3GzOvadBKsciUuIuzrxpBWZ2+3UcueVUQQ==", + "requires": { + "@polymer/iron-a11y-announcer": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-meta": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-meta/-/iron-meta-3.0.1.tgz", + "integrity": "sha512-pWguPugiLYmWFV9UWxLWzZ6gm4wBwQdDy4VULKwdHCqR7OP7u98h+XDdGZsSlDPv6qoryV/e3tGHlTIT0mbzJA==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-validatable-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-validatable-behavior/-/iron-validatable-behavior-3.0.1.tgz", + "integrity": "sha512-wwpYh6wOa4fNI+jH5EYKC7TVPYQ2OfgQqocWat7GsNWcsblKYhLYbwsvEY5nO0n2xKqNfZzDLrUom5INJN7msQ==", + "requires": { + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-input": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-input/-/paper-input-3.2.1.tgz", + "integrity": "sha512-6ghgwQKM6mS0hAQxQqj+tkeEY1VUBqAsrasAm8V5RpNcfSWQC/hhRFxU0beGuKTAhndzezDzWYP6Zz4b8fExGg==", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-autogrow-textarea": "^3.0.0-pre.26", + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-input": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-styles": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-styles/-/paper-styles-3.0.1.tgz", + "integrity": "sha512-y6hmObLqlCx602TQiSBKHqjwkE7xmDiFkoxdYGaNjtv4xcysOTdVJsDR/R9UHwIaxJ7gHlthMSykir1nv78++g==", + "requires": { + "@polymer/font-roboto": "^3.0.1", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/polymer": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.4.1.tgz", diff --git a/web/package.json b/web/package.json index 669cb25ec..8add33fd6 100644 --- a/web/package.json +++ b/web/package.json @@ -13,6 +13,7 @@ "@fortawesome/fontawesome-free": "^5.15.3", "@patternfly/patternfly": "^4.90.5", "@polymer/iron-form": "^3.0.1", + "@polymer/paper-input": "^3.2.1", "@sentry/browser": "^6.2.3", "@sentry/tracing": "^6.2.3", "@types/chart.js": "^2.9.31", diff --git a/web/src/elements/forms/HorizontalFormElement.ts b/web/src/elements/forms/HorizontalFormElement.ts new file mode 100644 index 000000000..20c03a174 --- /dev/null +++ b/web/src/elements/forms/HorizontalFormElement.ts @@ -0,0 +1,62 @@ +import { customElement, LitElement, CSSResult, property, css } from "lit-element"; +import { TemplateResult, html } from "lit-html"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; + +@customElement("ak-form-element-horizontal") +export class HorizontalFormElement extends LitElement { + + static get styles(): CSSResult[] { + return [PFForm, PFFormControl, css` + slot { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-around; + } + .pf-c-form__group { + display: grid; + grid-template-columns: var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth) var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth); + } + .pf-c-form__group-label { + padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop); + } + `]; + } + + @property() + label: string = ""; + + @property({ type: Boolean }) + required = false; + + @property() + errorMessage: string = ""; + + @property() + invalid: boolean = false; + + updated(): void { + this.querySelectorAll("input[autofocus]").forEach(input => { + input.focus(); + }); + } + + render(): TemplateResult { + return html`
+
+ +
+
+
+ + ${this.invalid ? html`

${this.errorMessage}

` : html``} +
+
+
`; + } + +} diff --git a/web/src/elements/forms/utils.ts b/web/src/elements/forms/utils.ts deleted file mode 100644 index 79ff0dc68..000000000 --- a/web/src/elements/forms/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TemplateResult, html } from "lit-html"; - -export function formGroup(label: string, body: TemplateResult): TemplateResult { - return html`
-
- -
-
-
- ${body} -
-
-
`; -} diff --git a/web/src/pages/groups/GroupForm.ts b/web/src/pages/groups/GroupForm.ts index 996f016d4..0770351c8 100644 --- a/web/src/pages/groups/GroupForm.ts +++ b/web/src/pages/groups/GroupForm.ts @@ -5,8 +5,8 @@ import { html, TemplateResult } from "lit-html"; import { DEFAULT_CONFIG } from "../../api/Config"; import { Form } from "../../elements/forms/Form"; import { until } from "lit-html/directives/until"; -import { formGroup } from "../../elements/forms/utils"; import { ifDefined } from "lit-html/directives/if-defined"; +import "../../elements/forms/HorizontalFormElement"; @customElement("ak-group-form") export class GroupForm extends Form { @@ -31,42 +31,42 @@ export class GroupForm extends Form { renderForm(): TemplateResult { return html`
- ${formGroup(gettext("Name"), html` - - `)} - ${formGroup("", html` -
- - -
-

${gettext("Users added to this group will be superusers.")}

- `)} - ${formGroup(gettext("Parent"), html` - + + +
+ + +
+

${gettext("Users added to this group will be superusers.")}

+
+ + + + + - `)} - ${formGroup(gettext("Members"), html` - -

${gettext("Hold control/command to select multiple items.")}

- `)} - `; + return html``; + }); + }))} + +

${gettext("Hold control/command to select multiple items.")}

+
+ `; } } From 12bfa404c847b9dbf24675af6f04e46800eb2b17 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 29 Mar 2021 11:57:53 +0200 Subject: [PATCH 16/60] web: rudimentary lazy loading for modals Signed-off-by: Jens Langhammer --- web/src/elements/buttons/ModalButton.ts | 5 +++++ web/src/elements/forms/Form.ts | 4 ++++ web/src/elements/forms/HorizontalFormElement.ts | 8 +------- web/src/pages/groups/GroupForm.ts | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/web/src/elements/buttons/ModalButton.ts b/web/src/elements/buttons/ModalButton.ts index 34061b451..a9e6703b2 100644 --- a/web/src/elements/buttons/ModalButton.ts +++ b/web/src/elements/buttons/ModalButton.ts @@ -136,6 +136,11 @@ export class ModalButton extends LitElement { if (!this.href) { this.updateHandlers(); this.open = true; + this.querySelectorAll("*").forEach(child => { + if ("requestUpdate" in child) { + (child as LitElement).requestUpdate(); + } + }); } else { const request = new Request(this.href); fetch(request, { diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index 5028579f9..bb7689e7e 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -100,6 +100,10 @@ export class Form extends LitElement { } render(): TemplateResult { + const rect = this.getBoundingClientRect(); + if (rect.x + rect.y + rect.width + rect.height === 0) { + return html``; + } return html` { this.submit(ev); }}> ${this.renderForm()} diff --git a/web/src/elements/forms/HorizontalFormElement.ts b/web/src/elements/forms/HorizontalFormElement.ts index 20c03a174..a38e4e3f5 100644 --- a/web/src/elements/forms/HorizontalFormElement.ts +++ b/web/src/elements/forms/HorizontalFormElement.ts @@ -8,12 +8,6 @@ export class HorizontalFormElement extends LitElement { static get styles(): CSSResult[] { return [PFForm, PFFormControl, css` - slot { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-around; - } .pf-c-form__group { display: grid; grid-template-columns: var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth) var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth); @@ -51,8 +45,8 @@ export class HorizontalFormElement extends LitElement {
+
- ${this.invalid ? html`

${this.errorMessage}

` : html``}
diff --git a/web/src/pages/groups/GroupForm.ts b/web/src/pages/groups/GroupForm.ts index 0770351c8..69f87619f 100644 --- a/web/src/pages/groups/GroupForm.ts +++ b/web/src/pages/groups/GroupForm.ts @@ -34,7 +34,7 @@ export class GroupForm extends Form { - +
diff --git a/web/src/pages/sources/SAMLSourceViewPage.ts b/web/src/pages/sources/SAMLSourceViewPage.ts index 7e4ca6dfa..d3e87052c 100644 --- a/web/src/pages/sources/SAMLSourceViewPage.ts +++ b/web/src/pages/sources/SAMLSourceViewPage.ts @@ -12,8 +12,6 @@ import PFFlex from "@patternfly/patternfly/utilities/Flex/flex.css"; import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; import AKGlobal from "../../authentik.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import CodeMirrorStyle from "codemirror/lib/codemirror.css"; -import CodeMirrorTheme from "codemirror/theme/monokai.css"; import "../../elements/buttons/ModalButton"; import "../../elements/buttons/SpinnerButton"; @@ -25,6 +23,7 @@ import { SAMLSource, SourcesApi } from "authentik-api"; import { DEFAULT_CONFIG } from "../../api/Config"; import { AdminURLManager, AppURLManager } from "../../api/legacy"; import { EVENT_REFRESH } from "../../constants"; +import { ifDefined } from "lit-html/directives/if-defined"; @customElement("ak-source-saml-view") export class SAMLSourceViewPage extends Page { @@ -51,7 +50,7 @@ export class SAMLSourceViewPage extends Page { source?: SAMLSource; static get styles(): CSSResult[] { - return [PFBase, PFPage, PFFlex, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, CodeMirrorStyle, CodeMirrorTheme, AKGlobal]; + return [PFBase, PFPage, PFFlex, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, AKGlobal]; } constructor() { @@ -138,7 +137,7 @@ export class SAMLSourceViewPage extends Page { ${until(new SourcesApi(DEFAULT_CONFIG).sourcesSamlMetadata({ slug: this.source.slug, }).then(m => { - return html``; + return html``; }) )} From 3cc7d54cc1fe4281c55ae07ca7a848e3941b3e02 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 29 Mar 2021 12:28:06 +0200 Subject: [PATCH 20/60] policies: use GroupSerializer for PolicyBinding API Signed-off-by: Jens Langhammer --- authentik/policies/api.py | 3 +++ swagger.yaml | 53 +-------------------------------------- 2 files changed, 4 insertions(+), 52 deletions(-) diff --git a/authentik/policies/api.py b/authentik/policies/api.py index 42ce8bc9f..d06740ab4 100644 --- a/authentik/policies/api.py +++ b/authentik/policies/api.py @@ -17,6 +17,7 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet from structlog.stdlib import get_logger from authentik.core.api.applications import user_app_cache_key +from authentik.core.api.groups import GroupSerializer from authentik.core.api.utils import ( CacheSerializer, MetaNameSerializer, @@ -176,6 +177,8 @@ class PolicyBindingSerializer(ModelSerializer): required=True, ) + group = GroupSerializer(required=False) + class Meta: model = PolicyBinding diff --git a/swagger.yaml b/swagger.yaml index da9fcb68f..264adfc9c 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -12216,58 +12216,7 @@ definitions: type: boolean readOnly: true group: - description: Custom Group model which supports a basic hierarchy - required: - - name - 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: - 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 + $ref: '#/definitions/Group' user: description: Custom User model to allow easier adding of user-based settings required: From 583b6cc20be23094d463a581bad2047a25367c4b Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 29 Mar 2021 14:42:28 +0200 Subject: [PATCH 21/60] web/admin: remove site-shell Signed-off-by: Jens Langhammer --- authentik/admin/views/utils.py | 10 -- web/src/pages/generic/SiteShell.ts | 173 ----------------------------- 2 files changed, 183 deletions(-) delete mode 100644 web/src/pages/generic/SiteShell.ts diff --git a/authentik/admin/views/utils.py b/authentik/admin/views/utils.py index c7ebe9eaf..5c5df69dd 100644 --- a/authentik/admin/views/utils.py +++ b/authentik/admin/views/utils.py @@ -11,16 +11,6 @@ from authentik.lib.utils.reflection import all_subclasses from authentik.lib.views import CreateAssignPermView -class DeleteMessageView(SuccessMessageMixin, DeleteView): - """DeleteView which shows `self.success_message` on successful deletion""" - - success_url = reverse_lazy("authentik_core:if-admin") - - def delete(self, request, *args, **kwargs): - messages.success(self.request, self.success_message) - return super().delete(request, *args, **kwargs) - - class InheritanceCreateView(CreateAssignPermView): """CreateView for objects using InheritanceManager""" diff --git a/web/src/pages/generic/SiteShell.ts b/web/src/pages/generic/SiteShell.ts deleted file mode 100644 index 34ab7eaf2..000000000 --- a/web/src/pages/generic/SiteShell.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; -import { SpinnerSize } from "../../elements/Spinner"; -import { showMessage } from "../../elements/messages/MessageContainer"; -import { gettext } from "django"; -import { SentryIgnoredError } from "../../common/errors"; -import { unsafeHTML } from "lit-html/directives/unsafe-html"; -import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import PFButton from "@patternfly/patternfly/components/Button/button.css"; -import PFModalBox from "@patternfly/patternfly/components/ModalBox/modal-box.css"; -import PFForm from "@patternfly/patternfly/components/Form/form.css"; -import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; -import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css"; -import PFBackdrop from "@patternfly/patternfly/components/Backdrop/backdrop.css"; -import PFPage from "@patternfly/patternfly/components/Page/page.css"; -import PFStack from "@patternfly/patternfly/layouts/Stack/stack.css"; -import PFCard from "@patternfly/patternfly/components/Card/card.css"; -import PFContent from "@patternfly/patternfly/components/Content/content.css"; -import AKGlobal from "../../authentik.css"; -import { EVENT_REFRESH } from "../../constants"; -import { MessageLevel } from "../../elements/messages/Message"; - -@customElement("ak-site-shell") -export class SiteShell extends LitElement { - @property() - set url(value: string) { - this._url = value; - this.loadContent(); - } - - _url?: string; - - @property({type: Boolean}) - loading = false; - - @property({type: String}) - body = ""; - - static get styles(): CSSResult[] { - return [PFBase, PFButton, PFModalBox, PFForm, PFFormControl, PFBullseye, PFBackdrop, PFPage, PFStack, PFCard, PFContent, AKGlobal].concat( - css` - :host, - ::slotted(*) { - height: 100%; - } - .pf-l-bullseye { - position: absolute; - top: 0; - left: 0; - width: 100%; - } - ` - ); - } - - constructor() { - super(); - this.addEventListener(EVENT_REFRESH, () => { - this.loadContent(); - }); - } - - loadContent(): void { - const bodySlot = this.querySelector("[slot=body]"); - if (!bodySlot) { - return; - } - if (!this._url) { - return; - } - if (this.loading) { - return; - } - this.loading = true; - fetch(this._url) - .then((response) => { - if (response.ok) { - return response; - } - console.debug(`authentik/site-shell: Request failed ${this._url}`); - showMessage({ - level: MessageLevel.error, - message: gettext(`Request failed: ${response.statusText}`), - }); - this.loading = false; - throw new SentryIgnoredError("Request failed"); - }) - .then((response) => response.text()) - .then((text) => { - this.body = text; - }) - .then(() => { - setTimeout(() => { - this.loading = false; - }, 100); - }); - } - - updateHandlers(): void { - // Ensure anchors only change the hash - this.shadowRoot?.querySelectorAll("a:not(.ak-root-link)").forEach((a) => { - if (a.href === "") { - return; - } - if (a.href.startsWith("#")) { - return; - } - try { - const url = new URL(a.href); - const qs = url.search || ""; - const hash = (url.hash || "#").substring(2, Infinity); - a.href = `#${url.pathname}${qs}${hash}`; - } catch (e) { - console.debug(`authentik/site-shell: error ${e}`); - a.href = `#${a.href}`; - } - }); - // Create refresh buttons - this.shadowRoot?.querySelectorAll("[role=ak-refresh]").forEach((rt) => { - rt.addEventListener("click", () => { - this.loadContent(); - }); - }); - // Make get forms (search bar) notify us on submit so we can change the hash - this.shadowRoot?.querySelectorAll("form[method=get]").forEach((form) => { - form.addEventListener("submit", (e) => { - e.preventDefault(); - const formData = new FormData(form); - const qs = new URLSearchParams((formData)).toString(); // eslint-disable-line - window.location.hash = `#${this._url}?${qs}`; - }); - }); - // Make forms with POST Method have a correct action set - this.shadowRoot?.querySelectorAll("form[method=post]").forEach((form) => { - form.addEventListener("submit", (e) => { - e.preventDefault(); - const formData = new FormData(form); - fetch(this._url ? this._url : form.action, { - method: form.method, - body: formData, - }) - .then((response) => { - return response.text(); - }) - .then((data) => { - this.body = data; - this.updateHandlers(); - }) - .catch((e) => { - showMessage({ - level: MessageLevel.error, - message: "Unexpected error" - }); - console.error(e); - }); - }); - }); - } - - render(): TemplateResult { - return html` ${this.loading ? - html`
-
- -
-
` - : ""} - ${unsafeHTML(this.body)}`; - } - - updated(): void { - this.updateHandlers(); - } -} From 0793fff2224ac03306a9aa2f2d64d5093ff77d3a Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 29 Mar 2021 15:36:35 +0200 Subject: [PATCH 22/60] *: simplify API permissions checking, add API for user recovery Signed-off-by: Jens Langhammer --- authentik/api/decorators.py | 28 ++++++++++++++ authentik/core/api/applications.py | 15 +++----- authentik/core/api/users.py | 38 ++++++++++++++++++- authentik/events/api/event.py | 4 +- .../events/api/notification_transport.py | 9 ++--- authentik/flows/api/flows.py | 19 ++++------ .../migrations/0017_auto_20210329_1334.py | 25 ++++++++++++ authentik/flows/models.py | 2 + authentik/policies/api.py | 3 ++ .../migrations/0006_auto_20210329_1334.py | 25 ++++++++++++ authentik/policies/models.py | 5 +++ swagger.yaml | 28 ++++++++++++++ 12 files changed, 173 insertions(+), 28 deletions(-) create mode 100644 authentik/api/decorators.py create mode 100644 authentik/flows/migrations/0017_auto_20210329_1334.py create mode 100644 authentik/policies/migrations/0006_auto_20210329_1334.py diff --git a/authentik/api/decorators.py b/authentik/api/decorators.py new file mode 100644 index 000000000..fd89c01dc --- /dev/null +++ b/authentik/api/decorators.py @@ -0,0 +1,28 @@ +"""API Decorators""" +from functools import wraps +from typing import Callable + +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet + + +def permission_required(perm: str, *other_perms: str): + """Check permissions for a single custom action""" + + def wrapper_outter(func: Callable): + """Check permissions for a single custom action""" + + @wraps(func) + def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response: + obj = self.get_object() + if not request.user.has_perm(perm, obj): + return self.permission_denied(request) + for other_perm in other_perms: + if not request.user.has_perm(other_perm): + return self.permission_denied(request) + return func(self, request, *args, **kwargs) + + return wrapper + + return wrapper_outter diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index eae9fc986..66076fe6c 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -1,12 +1,9 @@ """Application API Views""" from django.core.cache import cache from django.db.models import QuerySet -from django.http.response import Http404 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.fields import SerializerMethodField -from rest_framework.generics import get_object_or_404 from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ModelSerializer @@ -15,6 +12,7 @@ from rest_framework_guardian.filters import ObjectPermissionsFilter from structlog.stdlib import get_logger from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h +from authentik.api.decorators import permission_required from authentik.core.api.providers import ProviderSerializer from authentik.core.models import Application from authentik.events.models import EventAction @@ -110,16 +108,15 @@ class ApplicationViewSet(ModelViewSet): serializer = self.get_serializer(allowed_applications, many=True) return self.get_paginated_response(serializer.data) + @permission_required( + "authentik_core.view_application", "authentik_events.view_event" + ) @swagger_auto_schema(responses={200: CoordinateSerializer(many=True)}) @action(detail=True) + # pylint: disable=unused-argument def metrics(self, request: Request, slug: str): """Metrics for application logins""" - app = get_object_or_404( - get_objects_for_user(request.user, "authentik_core.view_application"), - slug=slug, - ) - if not request.user.has_perm("authentik_events.view_event"): - raise Http404 + app = self.get_object() return Response( get_events_per_1h( action=EventAction.AUTHORIZE_APPLICATION, diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index c8c04082c..2c9e0adcd 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -1,5 +1,7 @@ """User API Views""" from django.db.models.base import Model +from django.urls import reverse_lazy +from django.utils.http import urlencode from drf_yasg2.utils import swagger_auto_schema, swagger_serializer_method from guardian.utils import get_anonymous_user from rest_framework.decorators import action @@ -10,11 +12,12 @@ from rest_framework.serializers import BooleanField, ModelSerializer, Serializer from rest_framework.viewsets import ModelViewSet from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h +from authentik.api.decorators import permission_required from authentik.core.middleware import ( SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER, ) -from authentik.core.models import User +from authentik.core.models import Token, TokenIntents, User from authentik.events.models import EventAction @@ -54,6 +57,18 @@ class SessionUserSerializer(Serializer): raise NotImplementedError +class UserRecoverySerializer(Serializer): + """Recovery link for a user to reset their password""" + + link = CharField() + + def create(self, validated_data: dict) -> Model: + raise NotImplementedError + + def update(self, instance: Model, validated_data: dict) -> Model: + raise NotImplementedError + + class UserMetricsSerializer(Serializer): """User Metrics""" @@ -116,6 +131,7 @@ class UserViewSet(ModelViewSet): serializer.is_valid() return Response(serializer.data) + @permission_required("authentik_core.view_user", "authentik_events.view_event") @swagger_auto_schema(responses={200: UserMetricsSerializer(many=False)}) @action(detail=False) def metrics(self, request: Request) -> Response: @@ -123,3 +139,23 @@ class UserViewSet(ModelViewSet): serializer = UserMetricsSerializer(True) serializer.context["request"] = request return Response(serializer.data) + + @permission_required("authentik_core.reset_user_password") + @swagger_auto_schema( + responses={"200": UserRecoverySerializer(many=False)}, + ) + @action(detail=True) + # pylint: disable=invalid-name, unused-argument + def recovery(self, request: Request, pk: int) -> Response: + """Create a temporary link that a user can use to recover their accounts""" + user: User = self.get_object() + token, __ = Token.objects.get_or_create( + identifier=f"{user.uid}-password-reset", + user=user, + intent=TokenIntents.INTENT_RECOVERY, + ) + querystring = urlencode({"token": token.key}) + link = request.build_absolute_uri( + reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}" + ) + return Response({"link": link}) diff --git a/authentik/events/api/event.py b/authentik/events/api/event.py index 75cfbeae9..3eb7230bc 100644 --- a/authentik/events/api/event.py +++ b/authentik/events/api/event.py @@ -3,6 +3,7 @@ import django_filters from django.db.models.aggregates import Count from django.db.models.fields.json import KeyTextTransform 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.fields import CharField, DictField, IntegerField from rest_framework.request import Request @@ -132,7 +133,8 @@ class EventViewSet(ReadOnlyModelViewSet): filtered_action = request.query_params.get("action", EventAction.LOGIN) top_n = request.query_params.get("top_n", 15) return Response( - Event.objects.filter(action=filtered_action) + get_objects_for_user(request.user, "authentik_events.view_event") + .filter(action=filtered_action) .exclude(context__authorized_application=None) .annotate(application=KeyTextTransform("authorized_application", "context")) .annotate(user_pk=KeyTextTransform("pk", "user")) diff --git a/authentik/events/api/notification_transport.py b/authentik/events/api/notification_transport.py index e951f2a9f..b36b2dd71 100644 --- a/authentik/events/api/notification_transport.py +++ b/authentik/events/api/notification_transport.py @@ -1,7 +1,6 @@ """NotificationTransport API Views""" from django.http.response import Http404 from drf_yasg2.utils import no_body, swagger_auto_schema -from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action from rest_framework.fields import CharField, ListField, SerializerMethodField from rest_framework.request import Request @@ -9,6 +8,7 @@ from rest_framework.response import Response from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.viewsets import ModelViewSet +from authentik.api.decorators import permission_required from authentik.events.models import ( Notification, NotificationSeverity, @@ -57,18 +57,17 @@ class NotificationTransportViewSet(ModelViewSet): queryset = NotificationTransport.objects.all() serializer_class = NotificationTransportSerializer + @permission_required("authentik_events.change_notificationtransport") @swagger_auto_schema( responses={200: NotificationTransportTestSerializer(many=False)}, request_body=no_body, ) @action(detail=True, methods=["post"]) - # pylint: disable=invalid-name + # pylint: disable=invalid-name, unused-argument def test(self, request: Request, pk=None) -> Response: """Send example notification using selected transport. Requires Modify permissions.""" - transports = get_objects_for_user( - request.user, "authentik_events.change_notificationtransport" - ).filter(pk=pk) + transports = self.get_object() if not transports.exists(): raise Http404 transport: NotificationTransport = transports.first() diff --git a/authentik/flows/api/flows.py b/authentik/flows/api/flows.py index a87aff0b3..c2a886e4c 100644 --- a/authentik/flows/api/flows.py +++ b/authentik/flows/api/flows.py @@ -3,13 +3,11 @@ from dataclasses import dataclass from django.core.cache import cache from django.db.models import Model -from django.http.response import HttpResponseBadRequest, JsonResponse -from django.shortcuts import get_object_or_404 +from django.http.response import JsonResponse from drf_yasg2 import openapi from drf_yasg2.utils import no_body, swagger_auto_schema from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action -from rest_framework.exceptions import PermissionDenied from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ( @@ -21,6 +19,7 @@ from rest_framework.serializers import ( from rest_framework.viewsets import ModelViewSet from structlog.stdlib import get_logger +from authentik.api.decorators import permission_required from authentik.core.api.utils import CacheSerializer from authentik.flows.models import Flow from authentik.flows.planner import cache_key @@ -89,12 +88,14 @@ class FlowViewSet(ModelViewSet): search_fields = ["name", "slug", "designation", "title"] filterset_fields = ["flow_uuid", "name", "slug", "designation"] + @permission_required("authentik_flows.view_flow_cache") @swagger_auto_schema(responses={200: CacheSerializer(many=False)}) @action(detail=False) def cache_info(self, request: Request) -> Response: """Info about cached flows""" return Response(data={"count": len(cache.keys("flow_*"))}) + @permission_required("authentik_flows.clear_flow_cache") @swagger_auto_schema( request_body=no_body, responses={204: "Successfully cleared cache", 400: "Bad request"}, @@ -102,13 +103,12 @@ class FlowViewSet(ModelViewSet): @action(detail=False, methods=["POST"]) def cache_clear(self, request: Request) -> Response: """Clear flow cache""" - if not request.user.is_superuser: - return HttpResponseBadRequest() keys = cache.keys("flow_*") cache.delete_many(keys) LOGGER.debug("Cleared flow cache", keys=len(keys)) return Response(status=204) + @permission_required("authentik_flows.export_flow") @swagger_auto_schema( responses={ "200": openapi.Response( @@ -121,8 +121,6 @@ class FlowViewSet(ModelViewSet): def export(self, request: Request, slug: str) -> Response: """Export flow to .akflow file""" flow = self.get_object() - if not request.user.has_perm("authentik_flows.export_flow", flow): - raise PermissionDenied() exporter = FlowExporter(flow) response = JsonResponse(exporter.export(), encoder=DataclassEncoder, safe=False) response["Content-Disposition"] = f'attachment; filename="{flow.slug}.akflow"' @@ -130,13 +128,10 @@ class FlowViewSet(ModelViewSet): @swagger_auto_schema(responses={200: FlowDiagramSerializer()}) @action(detail=True, methods=["get"]) + # pylint: disable=unused-argument def diagram(self, request: Request, slug: str) -> Response: """Return diagram for flow with slug `slug`, in the format used by flowchart.js""" - flow = get_object_or_404( - get_objects_for_user(request.user, "authentik_flows.view_flow").filter( - slug=slug - ) - ) + flow = self.get_object() header = [ DiagramElement("st", "start", "Start"), ] diff --git a/authentik/flows/migrations/0017_auto_20210329_1334.py b/authentik/flows/migrations/0017_auto_20210329_1334.py new file mode 100644 index 000000000..bcaf18eef --- /dev/null +++ b/authentik/flows/migrations/0017_auto_20210329_1334.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.7 on 2021-03-29 13:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_flows", "0016_auto_20201202_1307"), + ] + + operations = [ + migrations.AlterModelOptions( + name="flow", + options={ + "permissions": [ + ("export_flow", "Can export a Flow"), + ("view_flow_cache", "View Flow's cache metrics"), + ("clear_flow_cache", "Clear Flow's cache metrics"), + ], + "verbose_name": "Flow", + "verbose_name_plural": "Flows", + }, + ), + ] diff --git a/authentik/flows/models.py b/authentik/flows/models.py index 136882acd..3db3db439 100644 --- a/authentik/flows/models.py +++ b/authentik/flows/models.py @@ -158,6 +158,8 @@ class Flow(SerializerModel, PolicyBindingModel): permissions = [ ("export_flow", "Can export a Flow"), + ("view_flow_cache", "View Flow's cache metrics"), + ("clear_flow_cache", "Clear Flow's cache metrics"), ] diff --git a/authentik/policies/api.py b/authentik/policies/api.py index 42ce8bc9f..c59cacfed 100644 --- a/authentik/policies/api.py +++ b/authentik/policies/api.py @@ -16,6 +16,7 @@ from rest_framework.serializers import ( from rest_framework.viewsets import GenericViewSet, ModelViewSet from structlog.stdlib import get_logger +from authentik.api.decorators import permission_required from authentik.core.api.applications import user_app_cache_key from authentik.core.api.utils import ( CacheSerializer, @@ -142,12 +143,14 @@ class PolicyViewSet( ) return Response(TypeCreateSerializer(data, many=True).data) + @permission_required("authentik_policies.view_policy_cache") @swagger_auto_schema(responses={200: CacheSerializer(many=False)}) @action(detail=False) def cache_info(self, request: Request) -> Response: """Info about cached policies""" return Response(data={"count": len(cache.keys("policy_*"))}) + @permission_required("authentik_policies.clear_policy_cache") @swagger_auto_schema( request_body=no_body, responses={204: "Successfully cleared cache", 400: "Bad request"}, diff --git a/authentik/policies/migrations/0006_auto_20210329_1334.py b/authentik/policies/migrations/0006_auto_20210329_1334.py new file mode 100644 index 000000000..e35b550a0 --- /dev/null +++ b/authentik/policies/migrations/0006_auto_20210329_1334.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.7 on 2021-03-29 13:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_policies", "0005_binding_group"), + ] + + operations = [ + migrations.AlterModelOptions( + name="policy", + options={ + "base_manager_name": "objects", + "permissions": [ + ("view_policy_cache", "View Policy's cache metrics"), + ("clear_policy_cache", "Clear Policy's cache metrics"), + ], + "verbose_name": "Policy", + "verbose_name_plural": "Policies", + }, + ), + ] diff --git a/authentik/policies/models.py b/authentik/policies/models.py index 9ad95422a..7e34131c3 100644 --- a/authentik/policies/models.py +++ b/authentik/policies/models.py @@ -149,3 +149,8 @@ class Policy(SerializerModel, CreatedUpdatedModel): verbose_name = _("Policy") verbose_name_plural = _("Policies") + + permissions = [ + ("view_policy_cache", "View Policy's cache metrics"), + ("clear_policy_cache", "Clear Policy's cache metrics"), + ] diff --git a/swagger.yaml b/swagger.yaml index da9fcb68f..27d16815e 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -1726,6 +1726,24 @@ paths: description: A unique integer value identifying this User. required: true type: integer + /core/users/{id}/recovery/: + get: + operationId: core_users_recovery + description: Create a temporary link that a user can use to recover their accounts + parameters: [] + responses: + '200': + description: Recovery link for a user to reset their password + schema: + $ref: '#/definitions/UserRecovery' + tags: + - core + parameters: + - name: id + in: path + description: A unique integer value identifying this User. + required: true + type: integer /crypto/certificatekeypairs/: get: operationId: crypto_certificatekeypairs_list @@ -11120,6 +11138,16 @@ definitions: items: $ref: '#/definitions/Coordinate' readOnly: true + UserRecovery: + description: Recovery link for a user to reset their password + required: + - link + type: object + properties: + link: + title: Link + type: string + minLength: 1 CertificateKeyPair: description: CertificateKeyPair Serializer required: From 0804b5e6c524157de6e320b59e2a4c667cace659 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 29 Mar 2021 15:53:56 +0200 Subject: [PATCH 23/60] web: shrink flow build by not including router Signed-off-by: Jens Langhammer --- web/src/constants.ts | 1 + web/src/elements/router/RouterOutlet.ts | 4 +--- web/src/flows/FlowExecutor.ts | 2 +- web/src/interfaces/AdminInterface.ts | 3 --- web/src/routes.ts | 1 - 5 files changed, 3 insertions(+), 8 deletions(-) diff --git a/web/src/constants.ts b/web/src/constants.ts index 04f480d9c..e21c53ad4 100644 --- a/web/src/constants.ts +++ b/web/src/constants.ts @@ -9,3 +9,4 @@ export const EVENT_REFRESH = "ak-refresh"; export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle"; export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle"; export const EVENT_API_DRAWER_REFRESH = "ak-api-drawer-refresh"; +export const TITLE_SUFFIX = "authentik"; diff --git a/web/src/elements/router/RouterOutlet.ts b/web/src/elements/router/RouterOutlet.ts index d702b389e..545117762 100644 --- a/web/src/elements/router/RouterOutlet.ts +++ b/web/src/elements/router/RouterOutlet.ts @@ -4,11 +4,9 @@ import { ROUTES } from "../../routes"; import { RouteMatch } from "./RouteMatch"; import AKGlobal from "../../authentik.css"; -import "../../pages/generic/SiteShell"; import "./Router404"; import { Page } from "../Page"; - -export const TITLE_SUFFIX = "authentik"; +import { TITLE_SUFFIX } from "../../constants"; @customElement("ak-router-outlet") export class RouterOutlet extends LitElement { diff --git a/web/src/flows/FlowExecutor.ts b/web/src/flows/FlowExecutor.ts index 10297e2b4..5e397f922 100644 --- a/web/src/flows/FlowExecutor.ts +++ b/web/src/flows/FlowExecutor.ts @@ -39,9 +39,9 @@ import { Challenge, ChallengeTypeEnum, Config, FlowsApi, RootApi } from "authent import { DEFAULT_CONFIG } from "../api/Config"; import { ifDefined } from "lit-html/directives/if-defined"; import { until } from "lit-html/directives/until"; -import { TITLE_SUFFIX } from "../elements/router/RouterOutlet"; import { AccessDeniedChallenge } from "./access_denied/FlowAccessDenied"; import { SpinnerSize } from "../elements/Spinner"; +import { TITLE_SUFFIX } from "../constants"; @customElement("ak-flow-executor") export class FlowExecutor extends LitElement implements StageHost { diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index 0418c459e..b710f7320 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -1,8 +1,5 @@ import "construct-style-sheets-polyfill"; -// Elements that are used by SiteShell pages -// And can't dynamically be imported -import "../elements/CodeMirror"; import "../elements/messages/MessageContainer"; import { customElement } from "lit-element"; import { me } from "../api/Users"; diff --git a/web/src/routes.ts b/web/src/routes.ts index ef93147b2..9e716cd90 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -11,7 +11,6 @@ import "./pages/events/RuleListPage"; import "./pages/events/TransportListPage"; import "./pages/flows/FlowListPage"; import "./pages/flows/FlowViewPage"; -import "./pages/generic/SiteShell"; import "./pages/groups/GroupListPage"; import "./pages/LibraryPage"; import "./pages/outposts/OutpostListPage"; From fac8d531636750a2209f7d568b5b241ce79e777e Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 29 Mar 2021 15:57:38 +0200 Subject: [PATCH 24/60] web/admin: fix message when object is created Signed-off-by: Jens Langhammer --- web/src/elements/forms/Form.ts | 6 +++++- web/src/pages/groups/GroupForm.ts | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index bb7689e7e..56b63c581 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -41,6 +41,10 @@ export class Form extends LitElement { `]; } + getSuccessMessage(): string { + return this.successMessage; + } + serializeForm(form: IronFormElement): T { const elements = form._getSubmittableElements(); const json: { [key: string]: unknown } = {}; @@ -69,7 +73,7 @@ export class Form extends LitElement { return this.send(data).then((r) => { showMessage({ level: MessageLevel.success, - message: this.successMessage + message: this.getSuccessMessage() }); return r; }).catch((ex: Response) => { diff --git a/web/src/pages/groups/GroupForm.ts b/web/src/pages/groups/GroupForm.ts index aa3c1adf6..57c46c36d 100644 --- a/web/src/pages/groups/GroupForm.ts +++ b/web/src/pages/groups/GroupForm.ts @@ -16,7 +16,13 @@ export class GroupForm extends Form { @property({attribute: false}) group?: Group; - successMessage = gettext("Successfully updated group"); + getSuccessMessage(): string { + if (this.group) { + return gettext("Successfully updated group"); + } else { + return gettext("Successfully created group"); + } + } send = (data: Group): Promise => { if (this.group) { From 526af265369372e14b43d011552b7d2e3c13f3cc Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 29 Mar 2021 16:16:27 +0200 Subject: [PATCH 25/60] web/admin: migrate user forms to web Signed-off-by: Jens Langhammer --- authentik/admin/forms/users.py | 22 ------- authentik/admin/urls.py | 9 --- authentik/admin/views/users.py | 74 ------------------------ authentik/admin/views/utils.py | 5 +- web/src/api/legacy.ts | 4 -- web/src/elements/buttons/ActionButton.ts | 3 +- web/src/pages/groups/GroupForm.ts | 4 +- web/src/pages/groups/GroupListPage.ts | 2 +- web/src/pages/users/UserForm.ts | 68 ++++++++++++++++++++++ web/src/pages/users/UserListPage.ts | 54 ++++++++++++----- web/src/pages/users/UserViewPage.ts | 44 +++++++++----- 11 files changed, 145 insertions(+), 144 deletions(-) delete mode 100644 authentik/admin/forms/users.py delete mode 100644 authentik/admin/views/users.py create mode 100644 web/src/pages/users/UserForm.ts diff --git a/authentik/admin/forms/users.py b/authentik/admin/forms/users.py deleted file mode 100644 index b7c3cc8d7..000000000 --- a/authentik/admin/forms/users.py +++ /dev/null @@ -1,22 +0,0 @@ -"""authentik administrative user forms""" - -from django import forms - -from authentik.admin.fields import CodeMirrorWidget, YAMLField -from authentik.core.models import User - - -class UserForm(forms.ModelForm): - """Update User Details""" - - class Meta: - - model = User - fields = ["username", "name", "email", "is_active", "attributes"] - widgets = { - "name": forms.TextInput, - "attributes": CodeMirrorWidget, - } - field_classes = { - "attributes": YAMLField, - } diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index 217145e3c..f581a0a89 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -18,7 +18,6 @@ from authentik.admin.views import ( stages_bindings, stages_invitations, stages_prompts, - users, ) from authentik.providers.saml.views.metadata import MetadataImportView @@ -152,14 +151,6 @@ urlpatterns = [ property_mappings.PropertyMappingTestView.as_view(), name="property-mapping-test", ), - # Users - path("users/create/", users.UserCreateView.as_view(), name="user-create"), - path("users//update/", users.UserUpdateView.as_view(), name="user-update"), - path( - "users//reset/", - users.UserPasswordResetView.as_view(), - name="user-password-reset", - ), # Certificate-Key Pairs path( "crypto/certificates/create/", diff --git a/authentik/admin/views/users.py b/authentik/admin/views/users.py deleted file mode 100644 index 760c67725..000000000 --- a/authentik/admin/views/users.py +++ /dev/null @@ -1,74 +0,0 @@ -"""authentik User administration""" -from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.mixins import ( - PermissionRequiredMixin as DjangoPermissionRequiredMixin, -) -from django.contrib.messages.views import SuccessMessageMixin -from django.http import HttpRequest, HttpResponse -from django.shortcuts import redirect -from django.urls import reverse_lazy -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 authentik.admin.forms.users import UserForm -from authentik.core.models import Token, User -from authentik.lib.views import CreateAssignPermView - - -class UserCreateView( - SuccessMessageMixin, - LoginRequiredMixin, - DjangoPermissionRequiredMixin, - CreateAssignPermView, -): - """Create user""" - - model = User - form_class = UserForm - permission_required = "authentik_core.add_user" - - template_name = "generic/create.html" - success_url = reverse_lazy("authentik_core:if-admin") - success_message = _("Successfully created User") - - -class UserUpdateView( - SuccessMessageMixin, - LoginRequiredMixin, - PermissionRequiredMixin, - UpdateView, -): - """Update user""" - - model = User - form_class = UserForm - permission_required = "authentik_core.change_user" - - # 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_core:if-admin") - success_message = _("Successfully updated User") - - -class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): - """Get Password reset link for user""" - - model = User - permission_required = "authentik_core.reset_user_password" - - def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - """Create token for user and return link""" - super().get(request, *args, **kwargs) - token, __ = Token.objects.get_or_create( - identifier="password-reset-temp", user=self.object - ) - querystring = urlencode({"token": token.key}) - link = request.build_absolute_uri( - reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}" - ) - messages.success(request, _("Password reset link: %(link)s" % {"link": link})) - return redirect("/") diff --git a/authentik/admin/views/utils.py b/authentik/admin/views/utils.py index 5c5df69dd..4c2fe7a38 100644 --- a/authentik/admin/views/utils.py +++ b/authentik/admin/views/utils.py @@ -1,11 +1,8 @@ """authentik admin util views""" from typing import Any -from django.contrib import messages -from django.contrib.messages.views import SuccessMessageMixin from django.http import Http404 -from django.urls import reverse_lazy -from django.views.generic import DeleteView, UpdateView +from django.views.generic import UpdateView from authentik.lib.utils.reflection import all_subclasses from authentik.lib.views import CreateAssignPermView diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts index 7e8504907..e921b689e 100644 --- a/web/src/api/legacy.ts +++ b/web/src/api/legacy.ts @@ -68,10 +68,6 @@ export class AdminURLManager { return `/administration/events/transports/${rest}`; } - static users(rest: string): string { - return `/administration/users/${rest}`; - } - } export class UserURLManager { diff --git a/web/src/elements/buttons/ActionButton.ts b/web/src/elements/buttons/ActionButton.ts index 6e3826f01..ed5e38263 100644 --- a/web/src/elements/buttons/ActionButton.ts +++ b/web/src/elements/buttons/ActionButton.ts @@ -23,8 +23,7 @@ export class ActionButton extends SpinnerButton { this.setLoading(); this.apiRequest().then(() => { this.setDone(SUCCESS_CLASS); - }) - .catch((e: Error | Response) => { + }).catch((e: Error | Response) => { if (e instanceof Error) { showMessage({ level: MessageLevel.error, diff --git a/web/src/pages/groups/GroupForm.ts b/web/src/pages/groups/GroupForm.ts index 57c46c36d..d173f9326 100644 --- a/web/src/pages/groups/GroupForm.ts +++ b/web/src/pages/groups/GroupForm.ts @@ -18,9 +18,9 @@ export class GroupForm extends Form { getSuccessMessage(): string { if (this.group) { - return gettext("Successfully updated group"); + return gettext("Successfully updated group."); } else { - return gettext("Successfully created group"); + return gettext("Successfully created group."); } } diff --git a/web/src/pages/groups/GroupListPage.ts b/web/src/pages/groups/GroupListPage.ts index d505e11a3..294750a69 100644 --- a/web/src/pages/groups/GroupListPage.ts +++ b/web/src/pages/groups/GroupListPage.ts @@ -66,7 +66,7 @@ export class GroupListPage extends TablePage { -
diff --git a/web/src/pages/users/UserForm.ts b/web/src/pages/users/UserForm.ts new file mode 100644 index 000000000..0cc8c565b --- /dev/null +++ b/web/src/pages/users/UserForm.ts @@ -0,0 +1,68 @@ +import { CoreApi, User } from "authentik-api"; +import { gettext } from "django"; +import { customElement, property } from "lit-element"; +import { html, TemplateResult } from "lit-html"; +import { DEFAULT_CONFIG } from "../../api/Config"; +import { Form } from "../../elements/forms/Form"; +import { ifDefined } from "lit-html/directives/if-defined"; +import "../../elements/forms/HorizontalFormElement"; +import "../../elements/CodeMirror"; +import YAML from "yaml"; + +@customElement("ak-user-form") +export class UserForm extends Form { + + @property({ attribute: false }) + user?: User; + + getSuccessMessage(): string { + if (this.user) { + return gettext("Successfully updated user."); + } else { + return gettext("Successfully created user."); + } + } + + send = (data: User): Promise => { + if (this.user) { + return new CoreApi(DEFAULT_CONFIG).coreUsersUpdate({ + id: this.user.pk || 0, + data: data + }); + } else { + return new CoreApi(DEFAULT_CONFIG).coreUsersCreate({ + data: data + }); + } + }; + + renderForm(): TemplateResult { + return html`
+ + +

${gettext("Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.")}

+
+ + +

${gettext("User's display name.")}

+
+ + + + +
+ + +
+

${gettext("Designates whether this user should be treated as active. Unselect this instead of deleting accounts.")}

+
+ + + + +
`; + } + +} diff --git a/web/src/pages/users/UserListPage.ts b/web/src/pages/users/UserListPage.ts index cdc0b4360..b0fd53486 100644 --- a/web/src/pages/users/UserListPage.ts +++ b/web/src/pages/users/UserListPage.ts @@ -3,16 +3,18 @@ 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/forms/ModalForm"; import "../../elements/buttons/Dropdown"; import "../../elements/buttons/ActionButton"; import { TableColumn } from "../../elements/table/Table"; import { PAGE_SIZE } from "../../constants"; import { CoreApi, User } from "authentik-api"; import { DEFAULT_CONFIG } from "../../api/Config"; -import { AdminURLManager } from "../../api/legacy"; import "../../elements/forms/DeleteForm"; import "./UserActiveForm"; +import "./UserForm"; +import { showMessage } from "../../elements/messages/MessageContainer"; +import { MessageLevel } from "../../elements/messages/Message"; @customElement("ak-user-list") export class UserListPage extends TablePage { @@ -59,12 +61,19 @@ export class UserListPage extends TablePage { html`${item.isActive ? "Yes" : "No"}`, html`${item.lastLogin?.toLocaleString()}`, html` - - + + + ${gettext("Update")} + + + ${gettext("Update User")} + + + + + + ${super.renderToolbar()} `; } + } diff --git a/web/src/pages/users/UserViewPage.ts b/web/src/pages/users/UserViewPage.ts index a1c59160a..e29acc762 100644 --- a/web/src/pages/users/UserViewPage.ts +++ b/web/src/pages/users/UserViewPage.ts @@ -12,7 +12,9 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; import AKGlobal from "../../authentik.css"; -import "../../elements/buttons/ModalButton"; +import "../../elements/forms/ModalForm"; +import "./UserForm"; +import "../../elements/buttons/ActionButton"; import "../../elements/buttons/SpinnerButton"; import "../../elements/CodeMirror"; import "../../elements/Tabs"; @@ -24,8 +26,9 @@ import "../../elements/charts/UserChart"; import { Page } from "../../elements/Page"; import { CoreApi, User } from "authentik-api"; import { DEFAULT_CONFIG } from "../../api/Config"; -import { AdminURLManager } from "../../api/legacy"; import { EVENT_REFRESH } from "../../constants"; +import { showMessage } from "../../elements/messages/MessageContainer"; +import { MessageLevel } from "../../elements/messages/Message"; @customElement("ak-user-view") export class UserViewPage extends Page { @@ -131,20 +134,35 @@ export class UserViewPage extends Page {