Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions authentik/providers/saml/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ def metadata(self, request: Request, pk: int) -> Response:
"multipart/form-data": SAMLProviderImportSerializer,
},
responses={
204: OpenApiResponse(description="Successfully imported provider"),
201: SAMLProviderSerializer,
400: OpenApiResponse(description="Bad request"),
},
)
Expand All @@ -330,17 +330,18 @@ def import_metadata(self, request: Request, body: SAMLProviderImportSerializer)
file.seek(0)
try:
metadata = ServiceProviderMetadataParser().parse(file.read().decode())
metadata.to_provider(
provider = metadata.to_provider(
body.validated_data["name"],
body.validated_data["authorization_flow"],
body.validated_data["invalidation_flow"],
)
# Return the created provider for use in workflows like the application wizard
return Response(SAMLProviderSerializer(provider).data, status=201)
except ValueError as exc: # pragma: no cover
LOGGER.warning(str(exc))
raise ValidationError(
_("Failed to import Metadata: {messages}".format_map({"messages": str(exc)})),
) from None
return Response(status=204)

@permission_required(
"authentik_providers_saml.view_samlprovider",
Expand Down
17 changes: 12 additions & 5 deletions authentik/providers/saml/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,21 +145,28 @@ def test_metadata_invalid(self):

def test_import_success(self):
"""Test metadata import (success case)"""
name = generate_id()
authorization_flow = create_test_flow(FlowDesignation.AUTHORIZATION)
invalidation_flow = create_test_flow(FlowDesignation.INVALIDATION)
with TemporaryFile() as metadata:
metadata.write(load_fixture("fixtures/simple.xml").encode())
metadata.seek(0)
response = self.client.post(
reverse("authentik_api:samlprovider-import-metadata"),
{
"file": metadata,
"name": generate_id(),
"authorization_flow": create_test_flow(FlowDesignation.AUTHORIZATION).pk,
"invalidation_flow": create_test_flow(FlowDesignation.INVALIDATION).pk,
"name": name,
"authorization_flow": authorization_flow.pk,
"invalidation_flow": invalidation_flow.pk,
},
format="multipart",
)
self.assertEqual(204, response.status_code)
# We don't test the actual object being created here, that has its own tests
self.assertEqual(201, response.status_code)
body = response.json()
self.assertIn("pk", body)
self.assertEqual(body["name"], name)
self.assertEqual(body["authorization_flow"], str(authorization_flow.pk))
self.assertEqual(body["invalidation_flow"], str(invalidation_flow.pk))

def test_import_failed(self):
"""Test metadata import (invalid xml)"""
Expand Down
8 changes: 6 additions & 2 deletions schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18431,8 +18431,12 @@ paths:
security:
- authentik: []
responses:
'204':
description: Successfully imported provider
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/SAMLProvider'
description: ''
'400':
description: Bad request
'403':
Expand Down
25 changes: 15 additions & 10 deletions web/src/admin/applications/wizard/steps/ProviderChoices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,38 @@ export const providerTypeRenderers: Record<
oauth2provider: {
render: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
order: 90,
order: 95,
},
ldapprovider: {
samlprovider: {
render: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
order: 70,
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
order: 90,
},
proxyprovider: {
samlproviderimportmodel: {
render: () =>
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
order: 75,
html`<ak-application-wizard-authentication-by-saml-metadata-configuration></ak-application-wizard-authentication-by-saml-metadata-configuration>`,
order: 85,
},
racprovider: {
render: () =>
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
order: 80,
},
samlprovider: {
proxyprovider: {
render: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
order: 80,
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
order: 75,
},
radiusprovider: {
render: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
order: 70,
},
ldapprovider: {
render: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
order: 65,
},
scimprovider: {
render: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
MatchingModeEnum,
OAuth2Provider,
ProviderModelEnum,
ProvidersSamlImportMetadataCreateRequest,
ProxyMode,
ProxyProvider,
RACProvider,
Expand Down Expand Up @@ -37,6 +38,15 @@ function renderSAMLOverview(rawProvider: OneOfProvider) {
]);
}

function renderSAMLImportOverview(rawProvider: OneOfProvider) {
const provider = rawProvider as ProvidersSamlImportMetadataCreateRequest;

return renderSummary("SAML", provider.name, [
[msg("Authorization flow"), provider.authorizationFlow ?? "-"],
[msg("Invalidation flow"), provider.invalidationFlow ?? "-"],
]);
}

function renderSCIMOverview(rawProvider: OneOfProvider) {
const provider = rawProvider as SCIMProvider;
return renderSummary("SCIM", provider.name, [[msg("URL"), provider.url]]);
Expand Down Expand Up @@ -149,6 +159,7 @@ const providerName = (p: ProviderModelEnum): string => p.toString().split(".")[1

export const providerRenderers = new Map([
[providerName(ProviderModelEnum.AuthentikProvidersSamlSamlprovider), renderSAMLOverview],
["samlproviderimportmodel", renderSAMLImportOverview],
[providerName(ProviderModelEnum.AuthentikProvidersScimScimprovider), renderSCIMOverview],
[providerName(ProviderModelEnum.AuthentikProvidersRadiusRadiusprovider), renderRadiusOverview],
[providerName(ProviderModelEnum.AuthentikProvidersRacRacprovider), renderRACOverview],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "./providers/ak-application-wizard-provider-for-proxy.js";
import "./providers/ak-application-wizard-provider-for-rac.js";
import "./providers/ak-application-wizard-provider-for-radius.js";
import "./providers/ak-application-wizard-provider-for-saml.js";
import "./providers/ak-application-wizard-provider-for-saml-metadata.js";
import "./providers/ak-application-wizard-provider-for-scim.js";

import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
Expand All @@ -24,6 +25,7 @@ const providerToTag = new Map([
["racprovider", "ak-application-wizard-provider-for-rac"],
["radiusprovider", "ak-application-wizard-provider-for-radius"],
["samlprovider", "ak-application-wizard-provider-for-saml"],
["samlproviderimportmodel", "ak-application-wizard-provider-for-saml-metadata"],
["scimprovider", "ak-application-wizard-provider-for-scim"],
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ import {
CoreApi,
instanceOfValidationError,
type ModelRequest,
PoliciesApi,
type PolicyBinding,
ProviderModelEnum,
ProvidersApi,
type ProvidersSamlImportMetadataCreateRequest,
ProxyMode,
type ProxyProviderRequest,
type SAMLProvider,
type TransactionApplicationRequest,
type TransactionApplicationResponse,
type TransactionPolicyBindingRequest,
Expand Down Expand Up @@ -100,6 +104,56 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
@state()
state: SubmitStates = "reviewing";

async sendSAMLMetadataImport() {
const providerData = this.wizard.provider as ProvidersSamlImportMetadataCreateRequest;
const providersApi = new ProvidersApi(DEFAULT_CONFIG);
const coreApi = new CoreApi(DEFAULT_CONFIG);
const policiesApi = new PoliciesApi(DEFAULT_CONFIG);

try {
// Step 1: Import SAML metadata to create the provider
const createdProvider = (await providersApi.providersSamlImportMetadataCreate({
file: providerData.file,
name: providerData.name,
authorizationFlow: providerData.authorizationFlow || "",
invalidationFlow: providerData.invalidationFlow || "",
})) as unknown as SAMLProvider;

// Step 2: Create the application linked to the provider
const appData = cleanApplication(this.wizard.app);
appData.provider = createdProvider.pk;

const createdApp = await coreApi.coreApplicationsCreate({
applicationRequest: appData,
});

// Step 3: Create policy bindings
for (const binding of this.wizard.bindings ?? []) {
const bindingData = cleanBinding(binding);
await policiesApi.policiesBindingsCreate({
policyBindingRequest: {
...bindingData,
target: createdApp.pk,
},
});
}

this.dispatchCustomEvent(EVENT_REFRESH);
this.state = "submitted";
} catch (error) {
const parsedError = await parseAPIResponseError(error);

if (!instanceOfValidationError(parsedError)) {
showAPIErrorMessage(parsedError);
this.state = "reviewing";
return;
}

this.handleUpdate({ errors: parsedError });
this.state = "reviewing";
}
}

async send() {
const app = this.wizard.app;
const provider = this.wizard.provider as ModelRequest;
Expand All @@ -112,6 +166,13 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
throw new Error("Reached the submit state with the provider undefined");
}

this.state = "running";

// Special case for SAML metadata import - use a two-step process
if (this.wizard.providerModel === "samlproviderimportmodel") {
return this.sendSAMLMetadataImport();
}

// Stringly-based API. Not the best, but it works. Just be aware that it is
// stringly-based.

Expand All @@ -133,8 +194,6 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
policyBindings: (this.wizard.bindings ?? []).map(cleanBinding),
};

this.state = "running";

return new CoreApi(DEFAULT_CONFIG)
.coreTransactionalApplicationsUpdate({
transactionApplicationRequest: request,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import "#admin/applications/wizard/ak-wizard-title";

import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";

import { createFileMap } from "#elements/utils/inputs";

import { renderForm } from "#admin/providers/saml/SAMLProviderImportFormForm";

import type { ProvidersSamlImportMetadataCreateRequest } from "@goauthentik/api";

import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { html } from "lit";

@customElement("ak-application-wizard-provider-for-saml-metadata")
export class ApplicationWizardProviderSamlMetadataForm extends ApplicationWizardProviderForm<ProvidersSamlImportMetadataCreateRequest> {
label = msg("Configure SAML Provider from Metadata");

override get formValues() {
const data = super.formValues;

// Get the file input separately since serializeForm doesn't handle files
const fileMap = createFileMap(this.form?.querySelectorAll("ak-form-element-horizontal"));
const file = fileMap.get("file");

if (file) {
data.file = file;
}

return data;
}

renderForm() {
return html`
<ak-wizard-title>${this.label}</ak-wizard-title>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm(this.wizard.provider)}
</form>
`;
}

render() {
if (!(this.wizard.provider && this.wizard.errors)) {
throw new Error("SAML Metadata Provider Step received uninitialized wizard context.");
}
return this.renderForm();
}
}

declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-provider-for-saml-metadata": ApplicationWizardProviderSamlMetadataForm;
}
}
48 changes: 6 additions & 42 deletions web/src/admin/providers/saml/SAMLProviderImportForm.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
import "#elements/forms/HorizontalFormElement";
import "#elements/forms/SearchSelect/index";
import { renderForm } from "./SAMLProviderImportFormForm.js";

import { DEFAULT_CONFIG } from "#common/api/config";
import { SentryIgnoredError } from "#common/sentry/index";

import { Form } from "#elements/forms/Form";

import { FlowsInstancesListDesignationEnum, ProvidersApi, SAMLProvider } from "@goauthentik/api";
import { ProvidersApi, SAMLProvider } from "@goauthentik/api";

import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement } from "lit/decorators.js";

@customElement("ak-provider-saml-import-form")
Expand All @@ -19,8 +16,8 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
return msg("Successfully imported provider.");
}

async send(data: SAMLProvider): Promise<void> {
const file = this.files().get("metadata");
async send(data: SAMLProvider): Promise<unknown> {
const file = this.files().get("file");
if (!file) {
throw new SentryIgnoredError("No form data");
}
Expand All @@ -32,41 +29,8 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
});
}

renderForm(): TemplateResult {
return html`<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input type="text" class="pf-c-form-control" required />
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
required
name="authorizationFlow"
>
<ak-flow-search-no-default
flowType=${FlowsInstancesListDesignationEnum.Authorization}
required
></ak-flow-search-no-default>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
required
name="invalidationFlow"
>
<ak-flow-search-no-default
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search-no-default>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>

<ak-form-element-horizontal label=${msg("Metadata")} name="metadata">
<input type="file" value="" class="pf-c-form-control" />
</ak-form-element-horizontal>`;
renderForm() {
return renderForm();
}
}

Expand Down
Loading
Loading