Skip to content

Commit

Permalink
Add generic OIDC provider support (rancher#11112)
Browse files Browse the repository at this point in the history
* Add generic oidc provider support

* Add oidc provider icon - filter genericoidc provider - update tests

* Automatically set scope - disable advanced inputs initially

* Update oidc login button with unique display name

* Remove default scope for oidc provider

* Add acrValue - move scope inputs

* Clean up validation - update wording - remove rogue c&p

Fix unit tests

* Remove required authEndpoint input

* Require authEndpoint for keycloakoidc - clean up validation
  • Loading branch information
jordojordo authored Jun 26, 2024
1 parent 4faf215 commit c6a56aa
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 37 deletions.
18 changes: 18 additions & 0 deletions shell/assets/images/vendor/openid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 11 additions & 1 deletion shell/assets/translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -580,11 +580,21 @@ authConfig:
label: Endpoints
custom: Specify
standard: Generate
url: URL
realm: Realm
keycloak:
url: Keycloak URL
realm: Keycloak Realm
issuer: Issuer
authEndpoint: Auth Endpoint
jwksUrl: JWKS URL
tokenEndpoint: Token Endpoint
userInfoEndpoint: User Info Endpoint
acrValue: Authorization Context Reference
scope:
label: Scopes
placeholder: openid
protip: The <code>openid</code> scope is required. If you are using custom scopes, ensure that it is included.
cert:
label: Certificate
placeholder: Paste in the certificate, starting with -----BEGIN CERTIFICATE-----
Expand Down Expand Up @@ -6511,7 +6521,7 @@ model:
okta: Okta
freeipa: FreeIPA
googleoauth: Google
oidc: OIDC
oidc: Generic OIDC
keycloakoidc: Keycloak

cluster:
Expand Down
8 changes: 7 additions & 1 deletion shell/components/auth/login/oidc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import Login from '@shell/mixins/login';
export default {
mixins: [Login],
computed: {
uniqueDisplayName() {
return this.t('model.authConfig.description.oidc');
},
},
methods: {
login() {
this.$store.dispatch('auth/redirectTo', { provider: this.name });
Expand All @@ -20,7 +26,7 @@ export default {
style="font-size: 18px;"
@click="login"
>
{{ t('login.loginWithProvider', {provider: displayName}) }}
{{ t('login.loginWithProvider', {provider: uniqueDisplayName}) }}
</button>
</div>
</template>
1 change: 1 addition & 0 deletions shell/config/product/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export function init(store) {
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/googleoauth`, 'auth/googleoauth');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/azuread`, 'auth/azuread');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/keycloakoidc`, 'auth/oidc');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/oidc`, 'auth/oidc');

basicType([
'config',
Expand Down
137 changes: 137 additions & 0 deletions shell/edit/auth/__tests__/oidc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/* eslint-disable jest/no-hooks */
import { mount } from '@vue/test-utils';
import { _EDIT } from '@shell/config/query-params';

import oidc from '@shell/edit/auth/oidc.vue';

jest.mock('@shell/utils/clipboard', () => {
return { copyTextToClipboard: jest.fn(() => Promise.resolve({})) };
});

const validClientId = 'rancheroidc';
const validClientSecret = 'TOkUxg0P67m1UXWNkJLHDPkUZFIKOWSq';
const validUrl = 'https://localhost:8080';
const validRealm = 'rancherrealm';
const validRancherUrl = 'https://localhost/verify-auth';
const validIssuer = 'http://localhost:8080/realms/rancherrealm';
const validAuthEndpoint = 'http://localhost:8080/realms/rancherrealm/protocol/openid-connect/auth';
const validScope = 'openid profile email';

const mockModel = {
enabled: false,
id: 'oidc',
rancherUrl: validRancherUrl,
issuer: validIssuer,
authEndpoint: validAuthEndpoint,
scope: validScope,
clientId: validClientId,
clientSecret: validClientSecret,
type: 'oidcConfig',
};

const mockedAuthConfigMixin = {
data() {
return {
isEnabling: false,
editConfig: false,
model: { ...mockModel },
serverSetting: null,
errors: [],
originalModel: null,
principals: [],
authConfigName: 'oidc',
};
},
computed: {},
methods: {}
};

describe('oidc.vue', () => {
let wrapper: any;
const requiredSetup = () => ({
mixins: [mockedAuthConfigMixin],
mocks: {
$fetchState: { pending: false },
$store: {
getters: {
currentStore: () => 'current_store',
'current_store/schemaFor': jest.fn(),
'current_store/all': jest.fn(),
'i18n/t': (val: string) => val,
'i18n/exists': jest.fn(),
},
dispatch: jest.fn()
},
$route: { query: { AS: '' }, params: { id: 'oicd' } },
$router: { applyQuery: jest.fn() },
},
propsData: {
value: { applicationSecret: '' },
mode: _EDIT,
},
});

beforeEach(() => {
wrapper = mount(oidc, { ...requiredSetup() });
});
afterEach(() => {
wrapper.destroy();
});

it('have "Create" button enabled when provider is enabled and not editing config', async() => {
wrapper.setData({ model: { enabled: true }, editConfig: false });
await wrapper.vm.$nextTick();

const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;

expect(saveButton.disabled).toBe(false);
});

it('have "Create" button disabled when provider is disabled and editing config before fields are filled in', async() => {
wrapper.setData({ model: {}, editConfig: true });
await wrapper.vm.$nextTick();

const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;

expect(saveButton.disabled).toBe(true);
});

it('have "Create" button disabled when provider is disabled and editing config after required fields and scope is missing openid', async() => {
wrapper.setData({ oidcUrls: { url: validUrl, realm: validRealm } });
await wrapper.vm.$nextTick();

const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;

expect(saveButton.disabled).toBe(true);
});

it('have "Create" button enabled when customEndpoint is disabled and required fields are filled in', async() => {
wrapper.setData({ oidcUrls: { url: validUrl, realm: validRealm }, oidcScope: validScope.split(' ') });
await wrapper.vm.$nextTick();

const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;

expect(saveButton.disabled).toBe(false);
});

it('have "Create" button enabled when customEndpoint is enabled and required fields are filled in', async() => {
wrapper.setData({ customEndpoint: { value: true }, oidcScope: validScope.split(' ') });
await wrapper.vm.$nextTick();

const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;

expect(saveButton.disabled).toBe(false);
});

it('updates issuer endpoint when oidcUrls.url and oidcUrls.realm changes', async() => {
wrapper.setData({ oidcUrls: { url: validUrl } });
await wrapper.vm.$nextTick();

expect(wrapper.vm.model.issuer).toBe(`${ validUrl }/realms/`);

wrapper.setData({ oidcUrls: { realm: validRealm } });
await wrapper.vm.$nextTick();

expect(wrapper.vm.model.issuer).toBe(`${ validUrl }/realms/${ validRealm }`);
});
});
Loading

0 comments on commit c6a56aa

Please sign in to comment.