Skip to content

Commit

Permalink
feat: add saml and attribute/mapper support for keycloak in uds pepr …
Browse files Browse the repository at this point in the history
…operator (#328)

## Description

Add support for saml protocol and attributes and protocolMappers support
for keycloak clients.

## Related Issue
Relates to #326 
Relates to #305

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Other (security config, docs update, etc)

## Checklist before merging

- [x] Test, docs, adr added or updated as needed
- [x] [Contributor Guide
Steps](https://github.com/defenseunicorns/uds-template-capability/blob/main/CONTRIBUTING.md)(https://github.com/defenseunicorns/uds-template-capability/blob/main/CONTRIBUTING.md#submitting-a-pull-request)
followed

---------

Co-authored-by: Rob Ferguson <rjferguson21@gmail.com>
Co-authored-by: Chance <139784371+UnicornChance@users.noreply.github.com>
Co-authored-by: Micah Nagel <micah.nagel@defenseunicorns.com>
  • Loading branch information
4 people committed Apr 19, 2024
1 parent 8485931 commit c53d4ee
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 1 deletion.
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

0 comments on commit c53d4ee

Please sign in to comment.