mirror of https://github.com/rancher/dashboard.git
Merge pull request #3064 from richard-cox/oidc-auth-provider
Add Keycloak OIDC provider
This commit is contained in:
commit
aada5c79a1
|
|
@ -228,7 +228,7 @@ authConfig:
|
|||
unrestricted: Allow any valid user
|
||||
allowedPrincipalIds:
|
||||
title: Authorized Users & Groups
|
||||
associatedWarning: 'Note: The {provider} user you authenticate as will be associated as an alternate way to login to the {vendor} user you are currently logged in as (<code>{username}</code>); all the global permissions, project, and cluster role bindings of this {vendor} user will also apply to the {provider} user.'
|
||||
associatedWarning: 'Note: The {provider} user you authenticate as will be associated as an alternate way to login to the {vendor} user you are currently logged in as <code>{username}</code>; all the global permissions, project, and cluster role bindings of this {vendor} user will also apply to the {provider} user.'
|
||||
github:
|
||||
clientId:
|
||||
label: Client ID
|
||||
|
|
@ -376,6 +376,27 @@ authConfig:
|
|||
graphEndpoint: Graph Endpoint
|
||||
tokenEndpoint: Token Endpoint
|
||||
authEndpoint: Auth Endpoint
|
||||
oidc:
|
||||
oidc: Configure an OIDC account
|
||||
keycloakoidc: Configure a Keycloak OIDC account
|
||||
rancherUrl: Rancher URL
|
||||
clientId: Client ID
|
||||
clientSecret: Client Secret
|
||||
customEndpoint:
|
||||
label: Endpoints
|
||||
custom: Specify (advanced)
|
||||
standard: Generate
|
||||
keycloak:
|
||||
url: Keycloak URL
|
||||
realm: Keycloak Realm
|
||||
issuer: Issuer
|
||||
authEndpoint: Auth Endpoint
|
||||
cert:
|
||||
label: Certificate
|
||||
placeholder: Paste in the certificate, starting with -----BEGIN CERTIFICATE-----
|
||||
key:
|
||||
label: Private Key
|
||||
placeholder: Paste in the private key, typically starting with -----BEGIN RSA PRIVATE KEY-----
|
||||
stateBanner:
|
||||
disabled: 'The {provider} authentication provider is currently disabled.'
|
||||
enabled: 'The {provider} authentication provider is currently enabled.'
|
||||
|
|
@ -3912,6 +3933,10 @@ model:
|
|||
ldap: LDAP
|
||||
saml: SAML
|
||||
oauth: OAuth
|
||||
oidc: OIDC
|
||||
name:
|
||||
keycloak: Keycloak (SAML)
|
||||
keycloakoidc: Keycloak (OIDC)
|
||||
provider:
|
||||
system: System
|
||||
local: Local
|
||||
|
|
@ -3928,6 +3953,8 @@ model:
|
|||
okta: Okta
|
||||
freeipa: FreeIPA
|
||||
googleoauth: Google
|
||||
oidc: OIDC
|
||||
keycloakoidc: Keycloak
|
||||
|
||||
cluster:
|
||||
name: Cluster Name
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ export default {
|
|||
.step-number {
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--secondary);
|
||||
color: var(--body-bg);
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,14 +24,16 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
const principalId = escape(this.value).replace(/\//g, '%2F');
|
||||
|
||||
try {
|
||||
this.principal = await this.$store.dispatch('rancher/find', {
|
||||
type: NORMAN.PRINCIPAL,
|
||||
id: this.value,
|
||||
opt: { url: `/v3/principals/${ escape(this.value).replace(/\//g, '%2F') }` }
|
||||
opt: { url: `/v3/principals/${ principalId }` }
|
||||
});
|
||||
} catch (e) {
|
||||
// Meh...
|
||||
console.error('Failed to fetch principal', this.value, principalId); // eslint-disable-line no-console
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import Login from '@/mixins/login';
|
||||
|
||||
export default {
|
||||
mixins: [Login],
|
||||
|
||||
methods: {
|
||||
login() {
|
||||
this.$store.dispatch('auth/redirectTo', { provider: this.name });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<button ref="btn" class="btn bg-primary" style="font-size: 18px;" @click="login">
|
||||
{{ t('login.loginWithProvider', {provider: displayName}) }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -168,6 +168,7 @@ export function init(store) {
|
|||
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/adfs`, 'auth/saml');
|
||||
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/googleoauth`, 'auth/googleoauth');
|
||||
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/azuread`, 'auth/azuread');
|
||||
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/keycloakoidc`, 'auth/oidc');
|
||||
|
||||
basicType([
|
||||
'config',
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ data:
|
|||
|
||||
## Keycloak
|
||||
|
||||
### Developer Set Up
|
||||
### Developer Set Up (SAML)
|
||||
Use the steps below to set up a Keycloak instance for dev environments and configure an Auth Provider for it.
|
||||
|
||||
1. Bring up a local Keycloak instance in docker using the instructions at [here](https://www.keycloak.org/getting-started/getting-started-docker).
|
||||
|
|
@ -46,3 +46,16 @@ Use the steps below to set up a Keycloak instance for dev environments and confi
|
|||
| Certificate | See Private Key section above|
|
||||
| Metadata | For the SAML Metadata, download as per Rancher docs. Be sure to follow the `NOTE` instructions regarding `EntitiesDescriptor` and `EntityDescriptor`. For a better set of instructions see [step 6](https://gist.github.com/PhilipSchmid/506b33cd74ddef4064d30fba50635c5b)|
|
||||
|
||||
### Developer Set Up (OIDC)
|
||||
1. In Vue UI set up the Keycloak OIDC provider with the following values
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Client ID | Find via the keycloak console |
|
||||
| Client Secret | Find via the keycloak console (client's credentials tab) |
|
||||
| Private Key (optional) | |
|
||||
| Certificate (optional) | |
|
||||
| Keycloak URL | URL of keycloak instance (no path) |
|
||||
| Keycloak Realm | Find via the keycloak console (above menu on left or in path after /realms/) |
|
||||
|
||||
> The user used when enabling the provider must be an Admin or in a group
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,241 @@
|
|||
<script>
|
||||
import Loading from '@/components/Loading';
|
||||
import CreateEditView from '@/mixins/create-edit-view';
|
||||
import AuthConfig from '@/mixins/auth-config';
|
||||
import CruResource from '@/components/CruResource';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import Banner from '@/components/Banner';
|
||||
import AllowedPrincipals from '@/components/auth/AllowedPrincipals';
|
||||
import FileSelector from '@/components/form/FileSelector';
|
||||
import AuthBanner from '@/components/auth/AuthBanner';
|
||||
import RadioGroup from '@/components/form/RadioGroup';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Loading,
|
||||
CruResource,
|
||||
LabeledInput,
|
||||
Banner,
|
||||
AllowedPrincipals,
|
||||
FileSelector,
|
||||
AuthBanner,
|
||||
RadioGroup
|
||||
},
|
||||
|
||||
mixins: [CreateEditView, AuthConfig],
|
||||
data() {
|
||||
return {
|
||||
customEndpoint: {
|
||||
value: false,
|
||||
labels: [
|
||||
this.t('authConfig.oidc.customEndpoint.standard'),
|
||||
this.t('authConfig.oidc.customEndpoint.custom'),
|
||||
],
|
||||
options: [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
keycloak: {
|
||||
url: null,
|
||||
realm: null
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
tArgs() {
|
||||
return {
|
||||
baseUrl: this.serverSetting,
|
||||
provider: this.displayName,
|
||||
username: this.principal.loginName || this.principal.name,
|
||||
};
|
||||
},
|
||||
|
||||
toSave() {
|
||||
return {
|
||||
enabled: true,
|
||||
oidcConfig: this.model
|
||||
};
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
'keycloak.url'() {
|
||||
this.updateKeycloak();
|
||||
},
|
||||
'keycloak.realm'() {
|
||||
this.updateKeycloak();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateKeycloak() {
|
||||
const url = this.keycloak.url.replaceAll(' ', '');
|
||||
|
||||
this.model.issuer = `${ url }/auth/realms/${ this.keycloak.realm || '' }`;
|
||||
this.model.authEndpoint = `${ this.model.issuer || '' }/protocol/openid-connect/auth`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<div v-else>
|
||||
<CruResource
|
||||
:cancel-event="true"
|
||||
:done-route="doneRoute"
|
||||
:mode="mode"
|
||||
:resource="model"
|
||||
:subtypes="[]"
|
||||
:validation-passed="true"
|
||||
:finish-button-mode="model.enabled ? 'edit' : 'enable'"
|
||||
:can-yaml="false"
|
||||
:errors="errors"
|
||||
:show-cancel="showCancel"
|
||||
@error="e=>errors = e"
|
||||
@finish="save"
|
||||
@cancel="cancel"
|
||||
>
|
||||
<template v-if="model.enabled && !isEnabling && !editConfig">
|
||||
<AuthBanner :t-args="tArgs" :disable="disable" :edit="goToEdit">
|
||||
<template slot="rows">
|
||||
<tr><td>{{ t(`authConfig.oidc.rancherUrl`) }}: </td><td>{{ model.rancherUrl }}</td></tr>
|
||||
<tr><td>{{ t(`authConfig.oidc.clientId`) }}: </td><td>{{ model.clientId }}</td></tr>
|
||||
<tr><td>{{ t(`authConfig.oidc.issuer`) }}: </td><td>{{ model.issuer }}</td></tr>
|
||||
<tr><td>{{ t(`authConfig.oidc.authEndpoint`) }}: </td><td>{{ model.authEndpoint }}</td></tr>
|
||||
</template>
|
||||
</AuthBanner>
|
||||
|
||||
<hr />
|
||||
|
||||
<AllowedPrincipals :provider="NAME" :auth-config="model" :mode="mode" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<Banner v-if="!model.enabled" :label="t('authConfig.stateBanner.disabled', tArgs)" color="warning" />
|
||||
|
||||
<h3>{{ t(`authConfig.oidc.${NAME}`) }}</h3>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="model.clientId"
|
||||
:label="t(`authConfig.oidc.clientId`)"
|
||||
:mode="mode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="model.clientSecret"
|
||||
:label="t(`authConfig.oidc.clientSecret`)"
|
||||
:mode="mode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="model.privateKey"
|
||||
:label="t(`authConfig.oidc.key.label`)"
|
||||
:placeholder="t(`authConfig.oidc.key.placeholder`)"
|
||||
:mode="mode"
|
||||
type="multiline"
|
||||
/>
|
||||
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'privateKey', $event)" />
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="model.certificate"
|
||||
:label="t(`authConfig.oidc.cert.label`)"
|
||||
:placeholder="t(`authConfig.oidc.cert.placeholder`)"
|
||||
:mode="mode"
|
||||
type="multiline"
|
||||
/>
|
||||
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'certificate', $event)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model="customEndpoint.value"
|
||||
name="customEndpoint"
|
||||
label-key="authConfig.oidc.customEndpoint.label"
|
||||
:labels="customEndpoint.labels"
|
||||
:options="customEndpoint.options"
|
||||
>
|
||||
<template #label>
|
||||
<h4>{{ t('authConfig.oidc.customEndpoint.label') }}</h4>
|
||||
</template>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="keycloak.url"
|
||||
:label="t(`authConfig.oidc.keycloak.url`)"
|
||||
:mode="mode"
|
||||
:required="!customEndpoint.value"
|
||||
:disabled="customEndpoint.value"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="keycloak.realm"
|
||||
:label="t(`authConfig.oidc.keycloak.realm`)"
|
||||
:mode="mode"
|
||||
:required="!customEndpoint.value"
|
||||
:disabled="customEndpoint.value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="model.rancherUrl"
|
||||
:label="t(`authConfig.oidc.rancherUrl`)"
|
||||
:mode="mode"
|
||||
required
|
||||
:disabled="!customEndpoint.value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="model.issuer"
|
||||
:label="t(`authConfig.oidc.issuer`)"
|
||||
:mode="mode"
|
||||
required
|
||||
:disabled="!customEndpoint.value"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="model.authEndpoint"
|
||||
:label="t(`authConfig.oidc.authEndpoint`)"
|
||||
:mode="mode"
|
||||
required
|
||||
:disabled="!customEndpoint.value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CruResource>
|
||||
<div v-if="!model.enabled" class="row">
|
||||
<div class="col span-12">
|
||||
<Banner color="info" v-html="t('authConfig.associatedWarning', tArgs, true)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { _EDIT } from '@/config/query-params';
|
||||
import { NORMAN, MANAGEMENT } from '@/config/types';
|
||||
import { AFTER_SAVE_HOOKS, BEFORE_SAVE_HOOKS } from '@/mixins/child-hook';
|
||||
import { BASE_SCOPES } from '@/store/auth';
|
||||
import { addObject, findBy } from '@/utils/array';
|
||||
import { set } from '@/utils/object';
|
||||
import { exceptionToErrorsArray } from '@/utils/error';
|
||||
|
||||
export default {
|
||||
beforeCreate() {
|
||||
|
|
@ -40,6 +42,7 @@ export default {
|
|||
this.$set(this.model, 'rancherApiHost', this.serverUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.model.enabled) {
|
||||
this.applyDefaults();
|
||||
}
|
||||
|
|
@ -125,11 +128,13 @@ export default {
|
|||
}
|
||||
try {
|
||||
if (this.editConfig || !wasEnabled) {
|
||||
if (configType === 'oauth') {
|
||||
if (configType === 'oauth' || configType === 'oidc') {
|
||||
const code = await this.$store.dispatch('auth/test', { provider: this.model.id, body: this.model });
|
||||
|
||||
obj.code = code;
|
||||
} if (configType === 'saml') {
|
||||
}
|
||||
|
||||
if (configType === 'saml') {
|
||||
if (!this.model.accessMode) {
|
||||
this.model.accessMode = 'unrestricted';
|
||||
}
|
||||
|
|
@ -146,15 +151,23 @@ export default {
|
|||
}
|
||||
await this.model.doAction('testAndApply', obj, { redirectUnauthorized: false });
|
||||
}
|
||||
// Reload principals to get the new ones from the provider
|
||||
|
||||
// Reload principals to get the new ones from the provider (including 'me')
|
||||
this.principals = await this.$store.dispatch('rancher/findAll', {
|
||||
type: NORMAN.PRINCIPAL,
|
||||
opt: { url: '/v3/principals', force: true }
|
||||
});
|
||||
|
||||
this.model.allowedPrincipalIds = this.model.allowedPrincipalIds || [];
|
||||
if ( this.me && !this.model.allowedPrincipalIds.includes(this.me.id) ) {
|
||||
addObject(this.model.allowedPrincipalIds, this.me.id);
|
||||
|
||||
if ( this.me) {
|
||||
if (!this.model.allowedPrincipalIds.includes(this.me.id) ) {
|
||||
addObject(this.model.allowedPrincipalIds, this.me.id);
|
||||
}
|
||||
// Session has switched to new 'me', ensure we react
|
||||
this.$store.commit('auth/loggedInAs', this.me.id);
|
||||
} else {
|
||||
console.warn(`Unable to find principal marked as 'me'`); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
if (wasEnabled && configType === 'oauth') {
|
||||
|
|
@ -169,7 +182,7 @@ export default {
|
|||
|
||||
btnCb(true);
|
||||
} catch (err) {
|
||||
this.errors = Array.isArray(err) ? err : [err];
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
btnCb(false);
|
||||
this.model.enabled = wasEnabled;
|
||||
this.isEnabling = false;
|
||||
|
|
@ -187,6 +200,9 @@ export default {
|
|||
await clone.save();
|
||||
}
|
||||
await this.reloadModel();
|
||||
// Covers case where user disables... then enables in same visit to page
|
||||
this.applyDefaults();
|
||||
|
||||
this.showLdap = false;
|
||||
btnCb(true);
|
||||
} catch (err) {
|
||||
|
|
@ -226,6 +242,18 @@ export default {
|
|||
|
||||
applyDefaults() {
|
||||
switch (this.value.configType) {
|
||||
case 'oidc': {
|
||||
const serverUrl = this.serverUrl.endsWith('/') ? this.serverUrl.slice(0, this.serverUrl.length - 1) : this.serverUrl;
|
||||
|
||||
// AuthConfig
|
||||
set(this.model, 'accessMode', 'unrestricted'); // This should remain as unrestricted, enabling will fail otherwise
|
||||
|
||||
// KeyCloakOIDCConfig --> OIDCConfig
|
||||
set(this.model, 'rancherUrl', `${ serverUrl }/verify-auth`);
|
||||
set(this.model, 'scope', BASE_SCOPES.keycloakoidc[0]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'saml':
|
||||
set(this.model, 'accessMode', 'unrestricted');
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@ export const configType = {
|
|||
googleoauth: 'oauth',
|
||||
local: '',
|
||||
github: 'oauth',
|
||||
keycloakoidc: 'oidc'
|
||||
};
|
||||
|
||||
const imageOverrides = { keycloakoidc: 'keycloak' };
|
||||
|
||||
export default {
|
||||
_availableActions() {
|
||||
const out = this._standardActions;
|
||||
|
|
@ -32,6 +35,10 @@ export default {
|
|||
},
|
||||
|
||||
nameDisplay() {
|
||||
return this.$rootGetters['i18n/withFallback'](`model.authConfig.name."${ this.id }"`, null, this.provider);
|
||||
},
|
||||
|
||||
provider() {
|
||||
return this.$rootGetters['i18n/withFallback'](`model.authConfig.provider."${ this.id }"`, null, this.id);
|
||||
},
|
||||
|
||||
|
|
@ -44,7 +51,7 @@ export default {
|
|||
},
|
||||
|
||||
icon() {
|
||||
return require(`~/assets/images/vendor/${ this.id }.svg`);
|
||||
return require(`~/assets/images/vendor/${ imageOverrides[this.id] || this.id }.svg`);
|
||||
},
|
||||
|
||||
state() {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,14 @@ export default {
|
|||
},
|
||||
|
||||
principal() {
|
||||
return this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, this.$store.getters['auth/principalId']) || {};
|
||||
const principalId = this.$store.getters['auth/principalId'];
|
||||
const principal = this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, principalId);
|
||||
|
||||
if (!principal) {
|
||||
console.error('Failed to find principal with id: ', principalId); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
return principal || {};
|
||||
},
|
||||
|
||||
apiKeys() {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export default {
|
|||
<main>
|
||||
<h1 class="text-center mt-50">
|
||||
<span v-if="testing">
|
||||
Testing…
|
||||
Testing Configuration…
|
||||
</span>
|
||||
<span v-else>
|
||||
Logging In…
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export default {
|
|||
|
||||
methods: {
|
||||
colorFor(row) {
|
||||
const types = ['ldap', 'oauth', 'saml'];
|
||||
const types = ['ldap', 'oauth', 'saml', 'oidc'];
|
||||
|
||||
const idx = types.indexOf(row.configType);
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ export default {
|
|||
<SelectIconGrid
|
||||
:rows="rows"
|
||||
:color-for="colorFor"
|
||||
name-field="nameDisplay"
|
||||
name-field="provider"
|
||||
@clicked="(row) => goTo(row.id)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const _ALL_IF_AUTHED = 'allIfAuthed';
|
|||
export const _NONE = 'none';
|
||||
|
||||
export default {
|
||||
request({ dispatch, rootGetters }, opt) {
|
||||
async request({ dispatch, rootGetters }, opt) {
|
||||
// Handle spoofed types instead of making an actual request
|
||||
// Spoofing is handled here to ensure it's done for both yaml and form editing.
|
||||
// It became apparent that this was the only place that both intersected
|
||||
|
|
@ -23,8 +23,10 @@ export default {
|
|||
const id = rest.join('/'); // Cover case where id contains '/'
|
||||
const isApi = scheme === SPOOFED_API_PREFIX;
|
||||
const typemapGetter = id ? 'getSpoofedInstance' : 'getSpoofedInstances';
|
||||
|
||||
const schemas = rootGetters['cluster/all'](SCHEMA);
|
||||
const instance = rootGetters[`type-map/${ typemapGetter }`](type, id);
|
||||
// getters return async getSpoofedInstance/getSpoofedInstances fn
|
||||
const instance = await rootGetters[`type-map/${ typemapGetter }`](type, id);
|
||||
const data = isApi ? createYaml(schemas, type, instance) : instance;
|
||||
|
||||
return id && !isApi ? data : { data };
|
||||
|
|
|
|||
|
|
@ -1262,6 +1262,8 @@ export default {
|
|||
let field, key, val, displayKey;
|
||||
|
||||
for ( let i = 0 ; i < keys.length ; i++ ) {
|
||||
const fieldErrors = [];
|
||||
|
||||
key = keys[i];
|
||||
field = fields[key];
|
||||
val = get(data, key);
|
||||
|
|
@ -1291,15 +1293,15 @@ export default {
|
|||
}
|
||||
}
|
||||
if (fieldType === 'boolean') {
|
||||
validateBoolean(val, field, displayKey, this.$rootGetters, errors);
|
||||
validateBoolean(val, field, displayKey, this.$rootGetters, fieldErrors);
|
||||
} else {
|
||||
validateLength(val, field, displayKey, this.$rootGetters, errors);
|
||||
validateChars(val, field, displayKey, this.$rootGetters, errors);
|
||||
validateLength(val, field, displayKey, this.$rootGetters, fieldErrors);
|
||||
validateChars(val, field, displayKey, this.$rootGetters, fieldErrors);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.push(this.t('validation.required', { key: displayKey }));
|
||||
|
||||
if (fieldErrors.length > 0) {
|
||||
fieldErrors.push(this.t('validation.required', { key: displayKey }));
|
||||
errors.push(...fieldErrors);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -1314,8 +1316,9 @@ export default {
|
|||
Vue.set(data, key, val);
|
||||
}
|
||||
|
||||
errors.push(...validateDnsLikeTypes(val, fieldType, displayKey, this.$rootGetters, errors));
|
||||
fieldErrors.push(...validateDnsLikeTypes(val, fieldType, displayKey, this.$rootGetters, fieldErrors));
|
||||
}
|
||||
errors.push(...fieldErrors);
|
||||
}
|
||||
|
||||
let { customValidationRules } = this;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import { GITHUB_SCOPE, GITHUB_NONCE, GITHUB_REDIRECT } from '@/config/query-para
|
|||
import { base64Encode } from '@/utils/crypto';
|
||||
|
||||
export const BASE_SCOPES = {
|
||||
github: ['read:org'], googleoauth: ['openid profile email'], azuread: []
|
||||
github: ['read:org'],
|
||||
googleoauth: ['openid profile email'],
|
||||
azuread: [],
|
||||
keycloakoidc: ['profile,email']
|
||||
};
|
||||
|
||||
const KEY = 'rc_nonce';
|
||||
|
|
@ -211,7 +214,7 @@ export const actions = {
|
|||
|
||||
return openAuthPopup(idpRedirectUrl, provider);
|
||||
} else {
|
||||
// github, google, azuread
|
||||
// github, google, azuread, oidc
|
||||
const res = await driver.doAction('configureTest', body);
|
||||
const { redirectUrl } = res;
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ export function returnTo(opt, vm) {
|
|||
export const authProvidersInfo = async(store) => {
|
||||
const rows = await store.dispatch(`management/findAll`, { type: MANAGEMENT.AUTH_CONFIG });
|
||||
const nonLocal = rows.filter(x => x.name !== 'local');
|
||||
// Generic OIDC is returned via API but not supported (and will be removed or fixed in future)
|
||||
const supportedNonLocal = nonLocal.filter(x => x.id !== 'oidc');
|
||||
const enabled = nonLocal.filter(x => x.enabled === true );
|
||||
|
||||
const enabledLocation = enabled.length === 1 ? {
|
||||
|
|
@ -72,7 +74,7 @@ export const authProvidersInfo = async(store) => {
|
|||
} : null;
|
||||
|
||||
return {
|
||||
nonLocal,
|
||||
nonLocal: supportedNonLocal,
|
||||
enabledLocation,
|
||||
enabled
|
||||
};
|
||||
|
|
|
|||
|
|
@ -41,9 +41,11 @@ export function stringify(err) {
|
|||
} else if ( err.url ) {
|
||||
str = `from ${ err.url }`;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!str) {
|
||||
// Good luck...
|
||||
str = `${ err }`;
|
||||
str = JSON.stringify(err);
|
||||
}
|
||||
|
||||
return str;
|
||||
|
|
|
|||
Loading…
Reference in New Issue