Merge branch 'main' into web/theme-controller-2

* main:
  sources/oauth: fix oidc well-known parsing (#7248)
  web/admin: improve user email button labels (#7233)
This commit is contained in:
Ken Sternberg 2023-10-20 14:12:57 -07:00
commit 8713a1d120
5 changed files with 123 additions and 118 deletions

View file

@ -71,15 +71,12 @@ class OAuthSourceSerializer(SourceSerializer):
text = exc.response.text if exc.response else str(exc)
raise ValidationError({"oidc_well_known_url": text})
config = well_known_config.json()
try:
attrs["authorization_url"] = config["authorization_endpoint"]
attrs["access_token_url"] = config["token_endpoint"]
attrs["profile_url"] = config["userinfo_endpoint"]
inferred_oidc_jwks_url = config["jwks_uri"]
except (IndexError, KeyError) as exc:
raise ValidationError(
{"oidc_well_known_url": f"Invalid well-known configuration: {exc}"}
)
if "issuer" not in config:
raise ValidationError({"oidc_well_known_url": "Invalid well-known configuration"})
attrs["authorization_url"] = config.get("authorization_endpoint", "")
attrs["access_token_url"] = config.get("token_endpoint", "")
attrs["profile_url"] = config.get("userinfo_endpoint", "")
inferred_oidc_jwks_url = config.get("jwks_uri", "")
# Prefer user-entered URL to inferred URL to default URL
jwks_url = attrs.get("oidc_jwks_url") or inferred_oidc_jwks_url or source_type.oidc_jwks_url

View file

@ -38,7 +38,7 @@ def update_well_known_jwks(self: MonitoredTask):
for source_attr, config_key in source_attr_key:
# Check if we're actually changing anything to only
# save when something has changed
if getattr(source, source_attr) != config[config_key]:
if getattr(source, source_attr, "") != config[config_key]:
dirty = True
setattr(source, source_attr, config[config_key])
except (IndexError, KeyError) as exc:

View file

@ -50,6 +50,7 @@ class TestOAuthSource(TestCase):
def test_api_validate_openid_connect(self):
"""Test API validation (with OIDC endpoints)"""
openid_config = {
"issuer": "foo",
"authorization_endpoint": "http://mock/oauth/authorize",
"token_endpoint": "http://mock/oauth/token",
"userinfo_endpoint": "http://mock/oauth/userinfo",

View file

@ -21,10 +21,11 @@ import { getURLParam, updateURLParams } from "@goauthentik/elements/router/Route
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import { writeToClipboard } from "@goauthentik/elements/utils/writeToClipboard";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
@ -40,6 +41,56 @@ import {
UserPath,
} from "@goauthentik/api";
export const requestRecoveryLink = (user: User) =>
new CoreApi(DEFAULT_CONFIG)
.coreUsersRecoveryRetrieve({
id: user.pk,
})
.then((rec) =>
writeToClipboard(rec.link).then((wroteToClipboard) =>
showMessage({
level: MessageLevel.success,
message: rec.link,
description: wroteToClipboard
? msg("A copy of this recovery link has been placed in your clipboard")
: "",
}),
),
)
.catch((ex: ResponseError) =>
ex.response.json().then(() =>
showMessage({
level: MessageLevel.error,
message: msg(
"The current tenant must have a recovery flow configured to use a recovery link",
),
}),
),
);
export const renderRecoveryEmailRequest = (user: User) =>
html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-email-recovery-request">
<span slot="submit"> ${msg("Send link")} </span>
<span slot="header"> ${msg("Send recovery link to user")} </span>
<ak-user-reset-email-form slot="form" .user=${user}> </ak-user-reset-email-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Email recovery link")}
</button>
</ak-forms-modal>`;
const recoveryButtonStyles = css`
#recovery-request-buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.375rem;
}
#recovery-request-buttons > *,
#update-password-request .pf-c-button {
margin: 0;
}
`;
@customElement("ak-user-list")
export class UserListPage extends TablePage<User> {
expandable = true;
@ -74,7 +125,7 @@ export class UserListPage extends TablePage<User> {
me?: SessionUser;
static get styles(): CSSResult[] {
return super.styles.concat(PFDescriptionList, PFCard, PFAlert);
return [...super.styles, PFDescriptionList, PFCard, PFAlert, recoveryButtonStyles];
}
constructor() {
@ -287,8 +338,14 @@ export class UserListPage extends TablePage<User> {
<span class="pf-c-description-list__text">${msg("Recovery")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-forms-modal size=${PFSize.Medium}>
<div
class="pf-c-description-list__text"
id="recovery-request-buttons"
>
<ak-forms-modal
size=${PFSize.Medium}
id="update-password-request"
>
<span slot="submit">${msg("Update password")}</span>
<span slot="header">${msg("Update password")}</span>
<ak-user-password-form
@ -303,56 +360,12 @@ export class UserListPage extends TablePage<User> {
? html`
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersRecoveryRetrieve({
id: item.pk,
})
.then((rec) => {
showMessage({
level: MessageLevel.success,
message: msg(
"Successfully generated recovery link",
),
description: rec.link,
});
})
.catch((ex: ResponseError) => {
ex.response.json().then(() => {
showMessage({
level: MessageLevel.error,
message: msg(
"No recovery flow is configured.",
),
});
});
});
}}
.apiRequest=${() => requestRecoveryLink(item)}
>
${msg("Copy recovery link")}
${msg("Create recovery link")}
</ak-action-button>
${item.email
? html`<ak-forms-modal
.closeAfterSuccessfulSubmit=${false}
>
<span slot="submit">
${msg("Send link")}
</span>
<span slot="header">
${msg("Send recovery link to user")}
</span>
<ak-user-reset-email-form
slot="form"
.user=${item}
>
</ak-user-reset-email-form>
<button
slot="trigger"
class="pf-c-button pf-m-secondary"
>
${msg("Email recovery link")}
</button>
</ak-forms-modal>`
? renderRecoveryEmailRequest(item)
: html`<span
>${msg(
"Recovery link cannot be emailed, user has no email address saved.",

View file

@ -5,11 +5,14 @@ import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserPasswordForm";
import "@goauthentik/app/admin/users/UserAssignedGlobalPermissionsTable";
import "@goauthentik/app/admin/users/UserAssignedObjectPermissionsTable";
import {
renderRecoveryEmailRequest,
requestRecoveryLink,
} from "@goauthentik/app/admin/users/UserListPage";
import { me } from "@goauthentik/app/common/users";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { MessageLevel } from "@goauthentik/common/messages";
import "@goauthentik/components/events/ObjectChangelog";
import "@goauthentik/components/events/UserEvents";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
@ -21,13 +24,12 @@ import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/ModalForm";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/oauth/UserRefreshList";
import "@goauthentik/elements/user/SessionList";
import "@goauthentik/elements/user/UserConsentList";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -72,7 +74,7 @@ export class UserViewPage extends AKElement {
@state()
me?: SessionUser;
static get styles(): CSSResult[] {
static get styles() {
return [
PFBase,
PFPage,
@ -84,12 +86,24 @@ export class UserViewPage extends AKElement {
PFDescriptionList,
PFSizing,
css`
.pf-c-description-list__description ak-action-button {
margin-right: 6px;
margin-bottom: 6px;
}
.ak-button-collection {
max-width: 12em;
display: flex;
flex-direction: column;
gap: 0.375rem;
max-width: 12rem;
}
.ak-button-collection > * {
flex: 1 0 100%;
}
#reset-password-button {
margin-right: 0;
}
#ak-email-recovery-request,
#update-password-request .pf-c-button,
#ak-email-recovery-request .pf-c-button {
margin: 0;
width: 100%;
}
`,
];
@ -103,7 +117,7 @@ export class UserViewPage extends AKElement {
});
}
render(): TemplateResult {
render() {
return html`<ak-page-header
icon="pf-icon pf-icon-user"
header=${msg(str`User ${this.user?.username || ""}`)}
@ -113,13 +127,17 @@ export class UserViewPage extends AKElement {
${this.renderBody()}`;
}
renderUserCard(): TemplateResult {
renderUserCard() {
if (!this.user) {
return html``;
return nothing;
}
const user = this.user;
const canImpersonate =
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
this.user.pk !== this.me?.user.pk;
return html`
<div class="pf-c-card__title">${msg("User Info")}</div>
<div class="pf-c-card__body">
@ -129,7 +147,7 @@ export class UserViewPage extends AKElement {
<span class="pf-c-description-list__text">${msg("Username")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${this.user.username}</div>
<div class="pf-c-description-list__text">${user.username}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
@ -137,7 +155,7 @@ export class UserViewPage extends AKElement {
<span class="pf-c-description-list__text">${msg("Name")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${this.user.name}</div>
<div class="pf-c-description-list__text">${user.name}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
@ -145,7 +163,7 @@ export class UserViewPage extends AKElement {
<span class="pf-c-description-list__text">${msg("Email")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${this.user.email || "-"}</div>
<div class="pf-c-description-list__text">${user.email || "-"}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
@ -154,7 +172,7 @@ export class UserViewPage extends AKElement {
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.user.lastLogin?.toLocaleString()}
${user.lastLogin?.toLocaleString()}
</div>
</dd>
</div>
@ -165,7 +183,7 @@ export class UserViewPage extends AKElement {
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-label
color=${this.user.isActive ? PFColor.Green : PFColor.Orange}
color=${user.isActive ? PFColor.Green : PFColor.Orange}
></ak-label>
</div>
</dd>
@ -177,7 +195,7 @@ export class UserViewPage extends AKElement {
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-label
color=${this.user.isSuperuser ? PFColor.Green : PFColor.Orange}
color=${user.isSuperuser ? PFColor.Green : PFColor.Orange}
></ak-label>
</div>
</dd>
@ -191,7 +209,7 @@ export class UserViewPage extends AKElement {
<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update User")} </span>
<ak-user-form slot="form" .instancePk=${this.user.pk}>
<ak-user-form slot="form" .instancePk=${user.pk}>
</ak-user-form>
<button
slot="trigger"
@ -201,13 +219,13 @@ export class UserViewPage extends AKElement {
</button>
</ak-forms-modal>
<ak-user-active-form
.obj=${this.user}
.obj=${user}
objectLabel=${msg("User")}
.delete=${() => {
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
id: this.user?.pk || 0,
id: user.pk,
patchedUserRequest: {
isActive: !this.user?.isActive,
isActive: !user.isActive,
},
});
}}
@ -218,15 +236,13 @@ export class UserViewPage extends AKElement {
>
<pf-tooltip
position="top"
content=${this.user.isActive
content=${user.isActive
? msg("Lock the user out of this system")
: msg(
"Allow the user to log in and use this system",
)}
>
${this.user.isActive
? msg("Deactivate")
: msg("Activate")}
${user.isActive ? msg("Deactivate") : msg("Activate")}
</pf-tooltip>
</button>
</ak-user-active-form>
@ -238,7 +254,7 @@ export class UserViewPage extends AKElement {
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: this.user?.pk || 0,
id: user.pk,
})
.then(() => {
window.location.href = "/";
@ -255,7 +271,7 @@ export class UserViewPage extends AKElement {
</pf-tooltip>
</ak-action-button>
`
: html``}
: nothing}
</div>
</dd>
</div>
@ -265,12 +281,12 @@ export class UserViewPage extends AKElement {
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text ak-button-collection">
<ak-forms-modal size=${PFSize.Medium}>
<ak-forms-modal size=${PFSize.Medium} id="update-password-request">
<span slot="submit">${msg("Update password")}</span>
<span slot="header">${msg("Update password")}</span>
<ak-user-password-form
slot="form"
.instancePk=${this.user?.pk}
.instancePk=${user.pk}
></ak-user-password-form>
<button
slot="trigger"
@ -287,30 +303,7 @@ export class UserViewPage extends AKElement {
<ak-action-button
id="reset-password-button"
class="pf-m-secondary pf-m-block"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersRecoveryRetrieve({
id: this.user?.pk || 0,
})
.then((rec) => {
showMessage({
level: MessageLevel.success,
message: msg(
"Successfully generated recovery link",
),
description: rec.link,
});
})
.catch(() => {
showMessage({
level: MessageLevel.error,
message: msg(
"To create a recovery link, the current tenant needs to have a recovery flow configured.",
),
description: "",
});
});
}}
.apiRequest=${() => requestRecoveryLink(user)}
>
<pf-tooltip
position="top"
@ -318,9 +311,10 @@ export class UserViewPage extends AKElement {
"Create a link for this user to reset their password",
)}
>
${msg("Reset Password")}
${msg("Create Recovery Link")}
</pf-tooltip>
</ak-action-button>
${user.email ? renderRecoveryEmailRequest(user) : nothing}
</div>
</dd>
</div>
@ -329,9 +323,9 @@ export class UserViewPage extends AKElement {
`;
}
renderBody(): TemplateResult {
renderBody() {
if (!this.user) {
return html``;
return nothing;
}
return html`<ak-tabs>
<section