Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add saml and attribute/mapper support for keycloak in uds pepr operator #328

Merged
merged 16 commits into from
Apr 19, 2024
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
37 changes: 36 additions & 1 deletion src/pepr/operator/controllers/keycloak/client-sync.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "@jest/globals";
import { generateSecretData } from "./client-sync";
import { extractSamlCertificateFromXML, generateSecretData } from "./client-sync";
import { Client } from "./types";

const mockClient: Client = {
Expand Down Expand Up @@ -60,6 +60,41 @@ const mockClientStringified: Record<string, string> = {
standardFlowEnabled: "true",
};

describe("Test XML Extraction Using Regex", () => {
it("extract xml", async () => {
// Sample XML string with namespace prefixes
const xmlString = `
<md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://keycloak.admin.uds.dev/realms/uds">
<md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:KeyName>SO1zm7gOpX2xlm16-pZ08zOJui0i7PwEHIqM6h4d9Sw</ds:KeyName>
<ds:X509Data>
<ds:X509Certificate>FOUND THE CERT</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://keycloak.admin.uds.dev/realms/uds/protocol/saml/resolve" index="0"></md:ArtifactResolutionService>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://keycloak.admin.uds.dev/realms/uds/protocol/saml"></md:SingleLogoutService>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://keycloak.admin.uds.dev/realms/uds/protocol/saml"></md:SingleLogoutService>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://keycloak.admin.uds.dev/realms/uds/protocol/saml"></md:SingleLogoutService>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://keycloak.admin.uds.dev/realms/uds/protocol/saml"></md:SingleLogoutService>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://keycloak.admin.uds.dev/realms/uds/protocol/saml"></md:SingleSignOnService>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://keycloak.admin.uds.dev/realms/uds/protocol/saml"></md:SingleSignOnService>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://keycloak.admin.uds.dev/realms/uds/protocol/saml"></md:SingleSignOnService>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://keycloak.admin.uds.dev/realms/uds/protocol/saml"></md:SingleSignOnService>
</md:IDPSSODescriptor>
</md:EntityDescriptor>
`;

expect(extractSamlCertificateFromXML(xmlString)).toEqual("FOUND THE CERT");
});
});

describe("Test Secret & Template Data Generation", () => {
it("generates data without template", async () => {
const expected: Record<string, string> = {};
Expand Down
31 changes: 31 additions & 0 deletions src/pepr/operator/controllers/keycloak/client-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,25 @@ import { Client } from "./types";

const apiURL =
"http://keycloak-http.keycloak.svc.cluster.local:8080/realms/uds/clients-registrations/default";
const samlDescriptorUrl =
"http://keycloak-http.keycloak.svc.cluster.local:8080/realms/uds/protocol/saml/descriptor";

// Template regex to match clientField() references, see https://regex101.com/r/e41Dsk/3 for details
const secretTemplateRegex = new RegExp(
'clientField\\(([a-zA-Z]+)\\)(?:\\["?([\\w]+)"?\\]|(\\.json\\(\\)))?',
"gm",
);

// Template regex to match IDPSSODescriptor in the SAML IDP Descriptor XML, see https://regex101.com/r/DGvzjd/1
const idpSSODescriptorRegex = new RegExp(
/<[^>]*:IDPSSODescriptor[^>]*>((.|[\n\r])*)<\/[^>]*:IDPSSODescriptor>/,
);

// Template regex to match the X509Certificate within the IDPSSODescriptor XML, see https://regex101.com/r/NjGZF5/1
const x509CertRegex = new RegExp(
/<[^>]*:X509Certificate[^>]*>((.|[\n\r])*)<\/[^>]*:X509Certificate>/,
);

/**
* Create or update the Keycloak clients for the package
*
Expand Down Expand Up @@ -89,6 +101,10 @@ async function syncClient(
// Remove the registrationAccessToken from the client object to avoid problems (one-time use token)
delete client.registrationAccessToken;

if (clientReq.protocol === "saml") {
client.samlIdpCertificate = await getSamlCertificate();
}

// Create or update the client secret
await K8s(kind.Secret).Apply({
metadata: {
Expand Down Expand Up @@ -191,6 +207,21 @@ export function generateSecretData(client: Client, secretTemplate?: { [key: stri
return stringMap;
}

export async function getSamlCertificate() {
const resp = await fetch<string>(samlDescriptorUrl);

if (!resp.ok) {
return undefined;
}

return extractSamlCertificateFromXML(resp.data);
}

export function extractSamlCertificateFromXML(xmlString: string) {
const extractedIDPSSODescriptor = xmlString.match(idpSSODescriptorRegex)?.[1] || "";
return extractedIDPSSODescriptor.match(x509CertRegex)?.[1] || "";
}

/**
* Process the secret template and convert the client data to base64 encoded strings for use in a secret
*
Expand Down
1 change: 1 addition & 0 deletions src/pepr/operator/controllers/keycloak/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export interface Client {
standardFlowEnabled: boolean;
surrogateAuthRequired: boolean;
webOrigins: string[];
samlIdpCertificate?: string;
}
16 changes: 16 additions & 0 deletions src/pepr/operator/crd/generated/package-v1alpha1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,10 @@ export interface Sso {
* session.
*/
alwaysDisplayInConsole?: boolean;
/**
* Specifies attributes for the client.
*/
attributes?: { [key: string]: string };
/**
* The client authenticator type
*/
Expand Down Expand Up @@ -449,6 +453,10 @@ export interface Sso {
* Specifies display name of the client
*/
name: string;
/**
* Specifies the protocol of the client, either 'openid-connect' or 'saml'
*/
protocol?: Protocol;
/**
* Valid URI pattern a browser can redirect to after a successful login. Simple wildcards
* are allowed such as 'https://unicorns.uds.dev/*'
Expand Down Expand Up @@ -485,6 +493,14 @@ export enum ClientAuthenticatorType {
ClientSecret = "client-secret",
}

/**
* Specifies the protocol of the client, either 'openid-connect' or 'saml'
*/
export enum Protocol {
OpenidConnect = "openid-connect",
Saml = "saml",
}

export interface Status {
endpoints?: string[];
networkPolicyCount?: number;
Expand Down
12 changes: 12 additions & 0 deletions src/pepr/operator/crd/sources/package/v1alpha1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,18 @@ const sso = {
"A description for the client, can be a URL to an image to replace the login logo",
type: "string",
},
protocol: {
description: "Specifies the protocol of the client, either 'openid-connect' or 'saml'",
type: "string",
enum: ["openid-connect", "saml"],
},
attributes: {
description: "Specifies attributes for the client.",
type: "object",
additionalProperties: {
type: "string",
},
},
rootUrl: {
description: "Root URL appended to relative URLs",
type: "string",
Expand Down
11 changes: 11 additions & 0 deletions tasks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ tasks:
echo " - Otherwise run 'npx pepr deploy' to deploy the Pepr module to the cluster"
echo " - Additional source packages can be deployed with 'zarf dev deploy src/<package>'"

- name: slim-dev
actions:
- description: "Create slim dev package"
task: create:slim-dev-package

- description: "Build slim dev bundle"
task: create:k3d-slim-dev-bundle

- description: "Deploy slim dev bundle"
task: deploy:k3d-slim-dev-bundle

- name: dev-deploy
actions:
- description: "Deploy the given source package with Zarf Dev"
Expand Down