Skip to content

Commit

Permalink
feat: identity group auth (#497)
Browse files Browse the repository at this point in the history
## Description
Identity Plugin Pepr component for managing group authorization to
applications. This is only the implementation, meaning a package (i.e.
grafana) will need to be configured for this to work. This is also
dependent on a new release of uds-identity-config.

## How to test
Update the identity config image
[here](https://github.com/defenseunicorns/uds-core/blob/main/src/keycloak/zarf.yaml#L24)
and
[here](https://github.com/defenseunicorns/uds-core/blob/main/src/keycloak/chart/values.yaml#L10)
to include/change :
`ttl.sh/uds-core-config:group-plugin-groups-attribute`

In addition to the identity config image, update the grafana package to
require a specific group to access the application like so:

```yaml
apiVersion: uds.dev/v1alpha1
kind: Package
metadata:
  name: grafana
  namespace: {{ .Release.Namespace }}
spec:
  sso:
    - name: Grafana Dashboard
      clientId: uds-core-admin-grafana
      redirectUris:
        - "https://grafana.admin.{{ .Values.domain }}/login/generic_oauth"
      groups:
        anyOf:
          - /UDS Core/Admin
```

With those changes utilize the `test-uds-core` task to test, no user is
created by default so either go into keycloak and setup the user
manually or go to `sso.uds.dev` and register a user there, then navigate
to the `grafana.admin.uds.dev` and you should be redirected to a login
page and depending on if you're in the admin group or not be granted
access to the grafana dashboard.

## Related Issue
Relates to `uds-identity-config`
[PR](defenseunicorns/uds-identity-config#107)

## 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
  • Loading branch information
UnicornChance authored Jun 28, 2024
1 parent 261057d commit d71d83e
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 6 deletions.
2 changes: 1 addition & 1 deletion src/keycloak/chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ image:
pullPolicy: IfNotPresent

# renovate: datasource=github-tags depName=defenseunicorns/uds-identity-config versioning=semver
configImage: ghcr.io/defenseunicorns/uds/identity-config:0.4.5
configImage: ghcr.io/defenseunicorns/uds/identity-config:0.5.0

# The public domain name of the Keycloak server
domain: "###ZARF_VAR_DOMAIN###"
Expand Down
4 changes: 2 additions & 2 deletions src/keycloak/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ components:
- "values/upstream-values.yaml"
images:
- quay.io/keycloak/keycloak:24.0.5
- ghcr.io/defenseunicorns/uds/identity-config:0.4.5
- ghcr.io/defenseunicorns/uds/identity-config:0.5.0

- name: keycloak
required: true
Expand All @@ -37,4 +37,4 @@ components:
- "values/registry1-values.yaml"
images:
- registry1.dso.mil/ironbank/opensource/keycloak/keycloak:24.0.5
- ghcr.io/defenseunicorns/uds/identity-config:0.4.5
- ghcr.io/defenseunicorns/uds/identity-config:0.5.0
96 changes: 95 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,10 @@
import { describe, expect, it } from "@jest/globals";
import { extractSamlCertificateFromXML, generateSecretData } from "./client-sync";
import { Sso } from "../../crd";
import {
extractSamlCertificateFromXML,
generateSecretData,
handleClientGroups,
} from "./client-sync";
import { Client } from "./types";

const mockClient: Client = {
Expand Down Expand Up @@ -132,3 +137,92 @@ describe("Test Secret & Template Data Generation", () => {
});
});
});

describe("handleClientGroups function", () => {
it('should correctly transform groups into attributes["uds.core.groups"]', () => {
// Arrange
const ssoWithGroups: Sso = {
clientId: "test-client",
name: "Test Client",
redirectUris: ["https://example.com/callback"],
groups: {
anyOf: ["group1", "group2"],
},
};

// Act
handleClientGroups(ssoWithGroups);

// Assert
expect(ssoWithGroups.attributes).toBeDefined();
expect(typeof ssoWithGroups.attributes).toBe("object");
expect(ssoWithGroups.attributes!["uds.core.groups"]).toEqual(
JSON.stringify({
anyOf: ["group1", "group2"],
}),
);
expect(ssoWithGroups.groups).toBeUndefined();
});

it('should set attributes["uds.core.groups"] to an empty object if groups are not provided', () => {
// Arrange
const ssoWithoutGroups: Sso = {
clientId: "test-client",
name: "Test Client",
redirectUris: ["https://example.com/callback"],
};

// Act
handleClientGroups(ssoWithoutGroups);

// Assert
expect(ssoWithoutGroups.attributes).toBeDefined();
expect(typeof ssoWithoutGroups.attributes).toBe("object");
expect(ssoWithoutGroups.attributes!["uds.core.groups"]).toEqual("");
expect(ssoWithoutGroups.groups).toBeUndefined();
});

it('should set attributes["uds.core.groups"] to an empty object if empty groups object is provided', () => {
// Arrange
const ssoWithoutGroups: Sso = {
clientId: "test-client",
name: "Test Client",
redirectUris: ["https://example.com/callback"],
groups: {},
};

// Act
handleClientGroups(ssoWithoutGroups);

// Assert
expect(ssoWithoutGroups.attributes).toBeDefined();
expect(typeof ssoWithoutGroups.attributes).toBe("object");
expect(ssoWithoutGroups.attributes!["uds.core.groups"]).toEqual("");
expect(ssoWithoutGroups.groups).toBeUndefined();
});

it('should set attributes["uds.core.groups"] to an empty array of groups if groups.anyOf is empty array', () => {
// Arrange
const ssoWithGroups: Sso = {
clientId: "test-client",
name: "Test Client",
redirectUris: ["https://example.com/callback"],
groups: {
anyOf: [],
},
};

// Act
handleClientGroups(ssoWithGroups);

// Assert
expect(ssoWithGroups.attributes).toBeDefined();
expect(typeof ssoWithGroups.attributes).toBe("object");
expect(ssoWithGroups.attributes!["uds.core.groups"]).toEqual(
JSON.stringify({
anyOf: [],
}),
);
expect(ssoWithGroups.groups).toBeUndefined();
});
});
17 changes: 17 additions & 0 deletions src/pepr/operator/controllers/keycloak/client-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ async function syncClient(

let client: Client;

handleClientGroups(clientReq);

// If an existing client is found, update it
if (token && !isRetry) {
Log.debug(pkg.metadata, `Found existing token for ${clientReq.clientId}`);
Expand Down Expand Up @@ -142,6 +144,21 @@ async function syncClient(
}
}

/**
* Handles the client groups by converting the groups to attributes.
* @param clientReq - The client request object.
*/
export function handleClientGroups(clientReq: Sso) {
if (clientReq.groups?.anyOf) {
clientReq.attributes = clientReq.attributes || {};
clientReq.attributes["uds.core.groups"] = JSON.stringify(clientReq.groups);
} else {
clientReq.attributes = clientReq.attributes || {};
clientReq.attributes["uds.core.groups"] = ""; // Remove groups attribute from client
}
delete clientReq.groups;
}

async function apiCall(sso: Partial<Sso>, method = "POST", authToken = "") {
// Handle single test mode
if (UDSConfig.isSingleTest) {
Expand Down
14 changes: 14 additions & 0 deletions src/pepr/operator/crd/generated/package-v1alpha1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,10 @@ export interface Sso {
* Whether the SSO client is enabled
*/
enabled?: boolean;
/**
* The client sso group type
*/
groups?: Groups;
/**
* If true, the client will generate a new Auth Service client as well
*/
Expand Down Expand Up @@ -526,6 +530,16 @@ export enum ClientAuthenticatorType {
ClientSecret = "client-secret",
}

/**
* The client sso group type
*/
export interface Groups {
/**
* List of group allowed to access to client
*/
anyOf?: string[];
}

/**
* Specifies the protocol of the client, either 'openid-connect' or 'saml'
*/
Expand Down
13 changes: 13 additions & 0 deletions src/pepr/operator/crd/sources/package/v1alpha1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,19 @@ const sso = {
type: "string",
},
},
groups: {
description: "The client sso group type",
type: "object",
properties: {
anyOf: {
description: "List of groups allowed to access to client",
type: "array",
items: {
type: "string",
},
},
},
},
},
} as V1JSONSchemaProps,
} as V1JSONSchemaProps;
Expand Down
4 changes: 2 additions & 2 deletions src/pepr/tasks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ tasks:
- name: gen-crds
description: "Generate CRDS, requires a running kubernetes cluster"
actions:
- cmd: "npx ts-node src/pepr/operator/crd/register.ts"
- cmd: npx ts-node -e "import { registerCRDs } from './src/pepr/operator/crd/register'; registerCRDs()"
env:
- "PEPR_WATCH_MODE=true"
- "PEPR_MODE=dev"

- cmd: "npx kubernetes-fluent-client crd packages.uds.dev src/pepr/operator/crd/generated"

Expand Down

0 comments on commit d71d83e

Please sign in to comment.