security: fix CVE 2022 23555 (#4274)
* add flow to invitation Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * show warning on invitation page Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add security advisory Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -9,8 +9,15 @@ import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { Invitation, StagesApi } from "@goauthentik/api"; | ||||
| import { | ||||
|     FlowsApi, | ||||
|     FlowsInstancesListDesignationEnum, | ||||
|     Invitation, | ||||
|     StagesApi, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-invitation-form") | ||||
| export class InvitationForm extends ModelForm<Invitation, string> { | ||||
| @ -66,6 +73,34 @@ export class InvitationForm extends ModelForm<Invitation, string> { | ||||
|                     value="${dateTimeLocal(first(this.instance?.expires, new Date()))}" | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Flow`} ?required=${true} name="flow"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option value="" ?selected=${this.instance?.flow === undefined}> | ||||
|                         --------- | ||||
|                     </option> | ||||
|                     ${until( | ||||
|                         new FlowsApi(DEFAULT_CONFIG) | ||||
|                             .flowsInstancesList({ | ||||
|                                 ordering: "slug", | ||||
|                                 designation: FlowsInstancesListDesignationEnum.Enrollment, | ||||
|                             }) | ||||
|                             .then((flows) => { | ||||
|                                 return flows.results.map((flow) => { | ||||
|                                     return html`<option | ||||
|                                         value=${ifDefined(flow.pk)} | ||||
|                                         ?selected=${this.instance?.flow === flow.pk} | ||||
|                                     > | ||||
|                                         ${flow.name} (${flow.slug}) | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`When selected, the invite will only be usable with the flow. By default the invite is accepted on all flows with invitation stages.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Attributes`} name="fixedData"> | ||||
|                 <ak-codemirror | ||||
|                     mode="yaml" | ||||
|  | ||||
| @ -14,12 +14,12 @@ import PFFormControl from "@patternfly/patternfly/components/FormControl/form-co | ||||
| import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { StagesApi } from "@goauthentik/api"; | ||||
| import { Invitation, StagesApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-stage-invitation-list-link") | ||||
| export class InvitationListLink extends AKElement { | ||||
|     @property() | ||||
|     invitation?: string; | ||||
|     @property({ attribute: false }) | ||||
|     invitation?: Invitation; | ||||
|  | ||||
|     @property() | ||||
|     selectedFlow?: string; | ||||
| @ -29,60 +29,67 @@ export class InvitationListLink extends AKElement { | ||||
|     } | ||||
|  | ||||
|     renderLink(): string { | ||||
|         return `${window.location.protocol}//${window.location.host}/if/flow/${this.selectedFlow}/?itoken=${this.invitation}`; | ||||
|         if (this.invitation?.flowObj) { | ||||
|             this.selectedFlow = this.invitation.flowObj?.slug; | ||||
|         } | ||||
|         return `${window.location.protocol}//${window.location.host}/if/flow/${this.selectedFlow}/?itoken=${this.invitation?.pk}`; | ||||
|     } | ||||
|  | ||||
|     renderFlowSelector(): TemplateResult { | ||||
|         return html`<div class="pf-c-description-list__group"> | ||||
|             <dt class="pf-c-description-list__term"> | ||||
|                 <span class="pf-c-description-list__text">${t`Select an enrollment flow`}</span> | ||||
|             </dt> | ||||
|             <dd class="pf-c-description-list__description"> | ||||
|                 <div class="pf-c-description-list__text"> | ||||
|                     <select | ||||
|                         class="pf-c-form-control" | ||||
|                         @change=${(ev: Event) => { | ||||
|                             const current = (ev.target as HTMLInputElement).value; | ||||
|                             this.selectedFlow = current; | ||||
|                         }} | ||||
|                     > | ||||
|                         ${until( | ||||
|                             new StagesApi(DEFAULT_CONFIG) | ||||
|                                 .stagesInvitationStagesList({ | ||||
|                                     ordering: "name", | ||||
|                                     noFlows: false, | ||||
|                                 }) | ||||
|                                 .then((stages) => { | ||||
|                                     if ( | ||||
|                                         !this.selectedFlow && | ||||
|                                         stages.results.length > 0 && | ||||
|                                         stages.results[0].flowSet | ||||
|                                     ) { | ||||
|                                         this.selectedFlow = stages.results[0].flowSet[0].slug; | ||||
|                                     } | ||||
|                                     const seenFlowSlugs: string[] = []; | ||||
|                                     return stages.results.map((stage) => { | ||||
|                                         return stage.flowSet?.map((flow) => { | ||||
|                                             if (seenFlowSlugs.includes(flow.slug)) { | ||||
|                                                 return html``; | ||||
|                                             } | ||||
|                                             seenFlowSlugs.push(flow.slug); | ||||
|                                             return html`<option | ||||
|                                                 value=${flow.slug} | ||||
|                                                 ?selected=${flow.slug === this.selectedFlow} | ||||
|                                             > | ||||
|                                                 ${flow.slug} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }); | ||||
|                                 }), | ||||
|                             html`<option>${t`Loading...`}</option>`, | ||||
|                         )} | ||||
|                     </select> | ||||
|                 </div> | ||||
|             </dd> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<dl class="pf-c-description-list pf-m-horizontal"> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`Select an enrollment flow`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text"> | ||||
|                         <select | ||||
|                             class="pf-c-form-control" | ||||
|                             @change=${(ev: Event) => { | ||||
|                                 const current = (ev.target as HTMLInputElement).value; | ||||
|                                 this.selectedFlow = current; | ||||
|                             }} | ||||
|                         > | ||||
|                             ${until( | ||||
|                                 new StagesApi(DEFAULT_CONFIG) | ||||
|                                     .stagesInvitationStagesList({ | ||||
|                                         ordering: "name", | ||||
|                                         noFlows: false, | ||||
|                                     }) | ||||
|                                     .then((stages) => { | ||||
|                                         if ( | ||||
|                                             !this.selectedFlow && | ||||
|                                             stages.results.length > 0 && | ||||
|                                             stages.results[0].flowSet | ||||
|                                         ) { | ||||
|                                             this.selectedFlow = stages.results[0].flowSet[0].slug; | ||||
|                                         } | ||||
|                                         const seenFlowSlugs: string[] = []; | ||||
|                                         return stages.results.map((stage) => { | ||||
|                                             return stage.flowSet?.map((flow) => { | ||||
|                                                 if (seenFlowSlugs.includes(flow.slug)) { | ||||
|                                                     return html``; | ||||
|                                                 } | ||||
|                                                 seenFlowSlugs.push(flow.slug); | ||||
|                                                 return html`<option | ||||
|                                                     value=${flow.slug} | ||||
|                                                     ?selected=${flow.slug === this.selectedFlow} | ||||
|                                                 > | ||||
|                                                     ${flow.slug} | ||||
|                                                 </option>`; | ||||
|                                             }); | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                     </div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|             ${this.invitation?.flow === undefined ? this.renderFlowSelector() : html``} | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text" | ||||
|  | ||||
| @ -2,6 +2,7 @@ import "@goauthentik/admin/stages/invitation/InvitationForm"; | ||||
| import "@goauthentik/admin/stages/invitation/InvitationListLink"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import "@goauthentik/elements/buttons/ModalButton"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| @ -18,7 +19,7 @@ import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | ||||
|  | ||||
| import { Invitation, StagesApi } from "@goauthentik/api"; | ||||
| import { FlowDesignationEnum, Invitation, StagesApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-stage-invitation-list") | ||||
| export class InvitationListPage extends TablePage<Invitation> { | ||||
| @ -49,12 +50,24 @@ export class InvitationListPage extends TablePage<Invitation> { | ||||
|     @state() | ||||
|     invitationStageExists = false; | ||||
|  | ||||
|     @state() | ||||
|     multipleEnrollmentFlows = false; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Invitation>> { | ||||
|         // Check if any invitation stages exist | ||||
|         const stages = await new StagesApi(DEFAULT_CONFIG).stagesInvitationStagesList({ | ||||
|             noFlows: false, | ||||
|         }); | ||||
|         this.invitationStageExists = stages.pagination.count > 0; | ||||
|         this.expandable = this.invitationStageExists; | ||||
|         stages.results.forEach((stage) => { | ||||
|             const enrollmentFlows = (stage.flowSet || []).filter( | ||||
|                 (flow) => flow.designation === FlowDesignationEnum.Enrollment, | ||||
|             ); | ||||
|             if (enrollmentFlows.length > 1) { | ||||
|                 this.multipleEnrollmentFlows = true; | ||||
|             } | ||||
|         }); | ||||
|         return new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
| @ -96,7 +109,14 @@ export class InvitationListPage extends TablePage<Invitation> { | ||||
|  | ||||
|     row(item: Invitation): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${item.name}`, | ||||
|             html`<div>${item.name}</div> | ||||
|                 ${!item.flowObj && this.multipleEnrollmentFlows | ||||
|                     ? html` | ||||
|                           <ak-label color=${PFColor.Orange}> | ||||
|                               ${t`Invitation not limited to any flow, and can be used with any enrollment flow.`} | ||||
|                           </ak-label> | ||||
|                       ` | ||||
|                     : html``}`, | ||||
|             html`${item.createdBy?.username}`, | ||||
|             html`${item.expires?.toLocaleString() || t`-`}`, | ||||
|             html` <ak-forms-modal> | ||||
| @ -114,7 +134,7 @@ export class InvitationListPage extends TablePage<Invitation> { | ||||
|         return html` <td role="cell" colspan="3"> | ||||
|                 <div class="pf-c-table__expandable-row-content"> | ||||
|                     <ak-stage-invitation-list-link | ||||
|                         invitation=${item.pk} | ||||
|                         .invitation=${item} | ||||
|                     ></ak-stage-invitation-list-link> | ||||
|                 </div> | ||||
|             </td> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L