diff --git a/authentik/api/v3/config.py b/authentik/api/v3/config.py index 91ef377a0d..75d6c60e39 100644 --- a/authentik/api/v3/config.py +++ b/authentik/api/v3/config.py @@ -31,6 +31,7 @@ class Capabilities(models.TextChoices): """Define capabilities which influence which APIs can/should be used""" CAN_SAVE_MEDIA = "can_save_media" + CAN_SAVE_REPORTS = "can_save_reports" CAN_GEO_IP = "can_geo_ip" CAN_ASN = "can_asn" CAN_IMPERSONATE = "can_impersonate" @@ -70,6 +71,8 @@ class ConfigView(APIView): caps = [] if get_file_manager(FileUsage.MEDIA).manageable: caps.append(Capabilities.CAN_SAVE_MEDIA) + if get_file_manager(FileUsage.REPORTS).manageable: + caps.append(Capabilities.CAN_SAVE_REPORTS) for processor in get_context_processors(): if cap := processor.capability(): caps.append(cap) diff --git a/schema.yml b/schema.yml index 56fe250fbd..f1857cfb52 100644 --- a/schema.yml +++ b/schema.yml @@ -34792,6 +34792,7 @@ components: CapabilitiesEnum: enum: - can_save_media + - can_save_reports - can_geo_ip - can_asn - can_impersonate diff --git a/web/src/admin/events/DataExportListPage.ts b/web/src/admin/events/DataExportListPage.ts index 1a12ebdc5c..64b6c53716 100644 --- a/web/src/admin/events/DataExportListPage.ts +++ b/web/src/admin/events/DataExportListPage.ts @@ -56,9 +56,16 @@ export class DataExportListPage extends TablePage { renderToolbarSelected(): TemplateResult { const disabled = this.selectedElements.length < 1; - return html` { + return [ + { key: msg("Data type"), value: item.contentType.verboseNamePlural }, + { key: msg("Requested by"), value: item.requestedBy.username }, + { key: msg("Creation date"), value: Timestamp(item.requestedOn) }, + ]; + }} .delete=${(item: DataExport) => { return new ReportsApi(DEFAULT_CONFIG).reportsExportsDestroy({ id: item.id, diff --git a/web/src/admin/files/FileListPage.ts b/web/src/admin/files/FileListPage.ts index 5f2704a43e..54f8e9ab5b 100644 --- a/web/src/admin/files/FileListPage.ts +++ b/web/src/admin/files/FileListPage.ts @@ -3,17 +3,20 @@ import "#elements/buttons/SpinnerButton/index"; import "#elements/forms/DeleteBulkForm"; import "#elements/forms/ModalForm"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; +import "#elements/EmptyState"; import { DEFAULT_CONFIG } from "#common/api/config"; +import { docLink } from "#common/global"; +import { WithCapabilitiesConfig } from "#elements/mixins/capabilities"; import { PaginatedResponse, TableColumn } from "#elements/table/Table"; import { TablePage } from "#elements/table/TablePage"; import { SlottedTemplateResult } from "#elements/types"; -import { AdminApi, AdminFileListUsageEnum } from "@goauthentik/api"; +import { AdminApi, AdminFileListUsageEnum, CapabilitiesEnum } from "@goauthentik/api"; import { msg } from "@lit/localize"; -import { html, TemplateResult } from "lit"; +import { html, nothing, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; export interface FileItem { @@ -25,7 +28,7 @@ export interface FileItem { export type FileListOrderKey = "name" | "mimeType"; @customElement("ak-files-list") -export class FileListPage extends TablePage { +export class FileListPage extends WithCapabilitiesConfig(TablePage) { public override checkbox = true; public override clearOnRefresh = true; @@ -67,7 +70,10 @@ export class FileListPage extends TablePage { [msg("Actions"), null, msg("Row Actions")], ]; - renderToolbarSelected(): TemplateResult { + renderToolbarSelected() { + if (!this.can(CapabilitiesEnum.CanSaveMedia)) { + return nothing; + } const disabled = !this.selectedElements.length; const count = this.selectedElements.length; return html` { ]; } - protected renderObjectCreate(): TemplateResult { + protected renderEmpty(inner?: TemplateResult) { + if (this.can(CapabilitiesEnum.CanSaveMedia)) { + return super.renderEmpty(inner); + } + return super.renderEmpty( + html`${msg("Configured file backend does not support file management.")} +
+ ${msg("Please ensure the data folder is mounted or S3 storage is configured.")} +
+ +
`, + ); + } + + protected renderObjectCreate() { + if (!this.can(CapabilitiesEnum.CanSaveMedia)) { + return nothing; + } return html` ${msg("Upload")} diff --git a/web/src/admin/reports/ExportButton.ts b/web/src/admin/reports/ExportButton.ts index 0b525edea2..1e48118142 100644 --- a/web/src/admin/reports/ExportButton.ts +++ b/web/src/admin/reports/ExportButton.ts @@ -1,10 +1,13 @@ import "#elements/forms/ConfirmationForm"; +import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import { parseAPIResponseError } from "#common/errors/network"; +import { docLink } from "#common/global"; import { AKElement } from "#elements/Base"; import { showAPIErrorMessage } from "#elements/messages/MessageContainer"; import { WithBrandConfig } from "#elements/mixins/branding"; +import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities"; import { WithLicenseSummary } from "#elements/mixins/license"; import { SlottedTemplateResult } from "#elements/types"; @@ -19,7 +22,9 @@ import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; @customElement("ak-reports-export-button") -export class ExportButton extends WithBrandConfig(WithLicenseSummary(AKElement)) { +export class ExportButton extends WithCapabilitiesConfig( + WithBrandConfig(WithLicenseSummary(AKElement)), +) { static styles: CSSResult[] = [PFButton, PFContent, PFDescriptionList]; @property({ attribute: false }) @@ -61,6 +66,37 @@ export class ExportButton extends WithBrandConfig(WithLicenseSummary(AKElement)) }); }; + renderBody() { + if (!this.can(CapabilitiesEnum.CanSaveReports)) { + return html`

+ ${msg( + "Data exports are not available as storage for reports is not configured.", + )} +

+ ${msg("Learn more")}`; + } + return html`

+ ${msg( + str`${this.brand.brandingTitle} will collect all objects with the specified parameters:`, + )} +

+
+ ${renderDescriptionList( + Object.keys(this.params) + .filter((key) => { + if (key === "page" || key === "pageSize") return false; + + return !!this.params[key]; + }) + .map((key): DescriptionPair => { + return [key, html`
${this.params[key]}
`]; + }), + { horizontal: true, compact: true }, + )}`; + } + render(): SlottedTemplateResult { if (!this.hasEnterpriseLicense) { return nothing; @@ -76,28 +112,10 @@ export class ExportButton extends WithBrandConfig(WithLicenseSummary(AKElement)) }} action=${msg("Start export")} actionLevel="pf-m-primary" + ?non-submittable=${!this.can(CapabilitiesEnum.CanSaveReports)} > ${msg("Export data")} -
-

- ${msg( - str`${this.brand.brandingTitle} will collect all objects with the specified parameters:`, - )} -

-
- ${renderDescriptionList( - Object.keys(this.params) - .filter((key) => { - if (key === "page" || key === "pageSize") return false; - - return !!this.params[key]; - }) - .map((key): DescriptionPair => { - return [key, html`
${this.params[key]}
`]; - }), - { horizontal: true, compact: true }, - )} -
+
${this.renderBody()}
diff --git a/web/src/elements/forms/ConfirmationForm.ts b/web/src/elements/forms/ConfirmationForm.ts index ad08de0bcd..5c4cae0a17 100644 --- a/web/src/elements/forms/ConfirmationForm.ts +++ b/web/src/elements/forms/ConfirmationForm.ts @@ -8,7 +8,7 @@ import { ModalButton } from "#elements/buttons/ModalButton"; import { showMessage } from "#elements/messages/MessageContainer"; import { msg, str } from "@lit/localize"; -import { html, TemplateResult } from "lit"; +import { html, nothing, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; @customElement("ak-forms-confirm") @@ -18,6 +18,9 @@ export class ConfirmationForm extends ModalButton { @property() errorMessage!: string; + @property({ type: Boolean, attribute: "non-submittable" }) + nonSubmittable = false; + @property() action!: string; @@ -75,14 +78,16 @@ export class ConfirmationForm extends ModalButton {