web/admin: prevent file upload attempt when backend not managed (#18646)

* web/admin: prevent file upload attempt when backend not managed

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* wip

* fixup

* rework

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add check for reports

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix delete table for data exports missing details

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
This commit is contained in:
Jens L.
2025-12-23 13:41:27 +01:00
committed by GitHub
parent d9cd1096b9
commit fcc0438961
7 changed files with 103 additions and 36 deletions
+3
View File
@@ -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)
+1
View File
@@ -34792,6 +34792,7 @@ components:
CapabilitiesEnum:
enum:
- can_save_media
- can_save_reports
- can_geo_ip
- can_asn
- can_impersonate
+8 -1
View File
@@ -56,9 +56,16 @@ export class DataExportListPage extends TablePage<DataExport> {
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html` <ak-forms-delete-bulk
return html`<ak-forms-delete-bulk
objectLabel=${msg("Data export(s)")}
.objects=${this.selectedElements}
.metadata=${(item: DataExport) => {
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,
+36 -5
View File
@@ -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<FileItem> {
export class FileListPage extends WithCapabilitiesConfig(TablePage<FileItem>) {
public override checkbox = true;
public override clearOnRefresh = true;
@@ -67,7 +70,10 @@ export class FileListPage extends TablePage<FileItem> {
[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`<ak-forms-delete-bulk
@@ -116,7 +122,32 @@ export class FileListPage extends TablePage<FileItem> {
];
}
protected renderObjectCreate(): TemplateResult {
protected renderEmpty(inner?: TemplateResult) {
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
return super.renderEmpty(inner);
}
return super.renderEmpty(
html`<ak-empty-state icon=${this.pageIcon}
><span>${msg("Configured file backend does not support file management.")}</span>
<div slot="body">
${msg("Please ensure the data folder is mounted or S3 storage is configured.")}
</div>
<div slot="primary">
<a
target="_blank"
class="pf-c-button pf-m-secondary"
href=${docLink("/install-config/configuration/#storage-settings")}
>${msg("Learn more")}</a
>
</div>
</ak-empty-state>`,
);
}
protected renderObjectCreate() {
if (!this.can(CapabilitiesEnum.CanSaveMedia)) {
return nothing;
}
return html`
<ak-forms-modal>
<span slot="submit">${msg("Upload")}</span>
+39 -21
View File
@@ -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`<p>
${msg(
"Data exports are not available as storage for reports is not configured.",
)}
</p>
<a href=${docLink("install-config/configuration/#storage-settings")}
>${msg("Learn more")}</a
>`;
}
return html`<p>
${msg(
str`${this.brand.brandingTitle} will collect all objects with the specified parameters:`,
)}
</p>
<br />
${renderDescriptionList(
Object.keys(this.params)
.filter((key) => {
if (key === "page" || key === "pageSize") return false;
return !!this.params[key];
})
.map((key): DescriptionPair => {
return [key, html`<pre>${this.params[key]}</pre>`];
}),
{ 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)}
>
<span slot="header">${msg("Export data")}</span>
<div slot="body">
<p>
${msg(
str`${this.brand.brandingTitle} will collect all objects with the specified parameters:`,
)}
</p>
<br />
${renderDescriptionList(
Object.keys(this.params)
.filter((key) => {
if (key === "page" || key === "pageSize") return false;
return !!this.params[key];
})
.map((key): DescriptionPair => {
return [key, html`<pre>${this.params[key]}</pre>`];
}),
{ horizontal: true, compact: true },
)}
</div>
<div slot="body">${this.renderBody()}</div>
<button slot="trigger" class="pf-c-button pf-m-secondary" type="button">
${msg("Export")}
</button>
+14 -9
View File
@@ -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 {
</form>
</section>
<footer class="pf-c-modal-box__footer">
<ak-spinner-button
.callAction=${() => {
return this.confirm();
}}
class=${this.actionLevel}
>
${this.action} </ak-spinner-button
>&nbsp;
${this.nonSubmittable
? nothing
: html`<ak-spinner-button
.callAction=${() => {
return this.confirm();
}}
class=${this.actionLevel}
>
${this.action} </ak-spinner-button
>&nbsp;`}
<ak-spinner-button
.callAction=${async () => {
this.open = false;
+2
View File
@@ -1,3 +1,5 @@
import "#elements/EmptyState";
import { updateURLParams } from "#elements/router/RouteMatch";
import { Table } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";