mirror of https://github.com/rancher/dashboard.git
539 lines
19 KiB
Vue
539 lines
19 KiB
Vue
<script>
|
|
import isEqual from 'lodash/isEqual';
|
|
import Loading from '@shell/components/Loading';
|
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
import FormValidation from '@shell/mixins/form-validation';
|
|
import CruResource from '@shell/components/CruResource';
|
|
import InfoBox from '@shell/components/InfoBox';
|
|
import { RadioGroup } from '@components/Form/Radio';
|
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
import AuthBanner from '@shell/components/auth/AuthBanner';
|
|
import CopyToClipboardText from '@shell/components/CopyToClipboardText.vue';
|
|
import AllowedPrincipals from '@shell/components/auth/AllowedPrincipals';
|
|
import AuthConfig from '@shell/mixins/auth-config';
|
|
import { AZURE_MIGRATED } from '@shell/config/labels-annotations';
|
|
import { get } from '@shell/utils/object';
|
|
import AuthProviderWarningBanners from '@shell/edit/auth/AuthProviderWarningBanners';
|
|
import formRulesGenerator from '@shell/utils/validators/formRules/index';
|
|
|
|
const TENANT_ID_TOKEN = '__[[TENANT_ID]]__';
|
|
|
|
// Azure AD Graph will be deprecated end of 2022, see: https://docs.microsoft.com/en-us/graph/migrate-azure-ad-graph-overview
|
|
export const OLD_ENDPOINTS = {
|
|
standard: {
|
|
graphEndpoint: 'https://graph.windows.net/',
|
|
tokenEndpoint: `https://login.microsoftonline.com/${ TENANT_ID_TOKEN }/oauth2/token`,
|
|
authEndpoint: `https://login.microsoftonline.com/${ TENANT_ID_TOKEN }/oauth2/authorize`
|
|
},
|
|
china: {
|
|
graphEndpoint: 'https://graph.chinacloudapi.cn/',
|
|
tokenEndpoint: `https://login.chinacloudapi.cn/${ TENANT_ID_TOKEN }/oauth2/token`,
|
|
authEndpoint: `https://login.chinacloudapi.cn/${ TENANT_ID_TOKEN }/oauth2/authorize`
|
|
}
|
|
};
|
|
|
|
const ENDPOINT_MAPPING = {
|
|
standard: {
|
|
endpoint: 'https://login.microsoftonline.com/',
|
|
graphEndpoint: 'https://graph.microsoft.com',
|
|
tokenEndpoint: `https://login.microsoftonline.com/${ TENANT_ID_TOKEN }/oauth2/v2.0/token`,
|
|
authEndpoint: `https://login.microsoftonline.com/${ TENANT_ID_TOKEN }/oauth2/v2.0/authorize`
|
|
},
|
|
china: {
|
|
endpoint: 'https://login.partner.microsoftonline.cn/',
|
|
graphEndpoint: 'https://microsoftgraph.chinacloudapi.cn',
|
|
tokenEndpoint: `https://login.partner.microsoftonline.cn/${ TENANT_ID_TOKEN }/oauth2/v2.0/token`,
|
|
authEndpoint: `https://login.partner.microsoftonline.cn/${ TENANT_ID_TOKEN }/oauth2/v2.0/authorize`
|
|
},
|
|
custom: {
|
|
endpoint: 'https://login.microsoftonline.com/',
|
|
graphEndpoint: '',
|
|
tokenEndpoint: '',
|
|
authEndpoint: ''
|
|
}
|
|
};
|
|
|
|
export default {
|
|
components: {
|
|
Loading,
|
|
CruResource,
|
|
InfoBox,
|
|
RadioGroup,
|
|
LabeledInput,
|
|
Checkbox,
|
|
CopyToClipboardText,
|
|
AllowedPrincipals,
|
|
AuthBanner,
|
|
AuthProviderWarningBanners
|
|
},
|
|
|
|
mixins: [CreateEditView, AuthConfig, FormValidation],
|
|
|
|
async fetch() {
|
|
await this.reloadModel();
|
|
|
|
if ( this.value?.graphEndpoint ) {
|
|
this.setInitialEndpoint(this.value.graphEndpoint);
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
isGroupMembershipFilterEnabled: !!this.value.groupMembershipFilter,
|
|
endpoint: 'standard',
|
|
oldEndpoint: false,
|
|
|
|
// Storing the applicationSecret is necessary because norman doesn't support returning secrets and when we
|
|
// override the steve authconfig with a norman config the applicationSecret is lost
|
|
applicationSecret: this.value.applicationSecret,
|
|
fvFormRuleSets: [
|
|
{ path: 'tenantId', rules: ['tenantIdRequired'] },
|
|
{ path: 'applicationId', rules: ['applicationIdRequired'] },
|
|
{ path: 'applicationSecret', rules: ['applicationSecretRequired'] },
|
|
{ path: 'endpoint', rules: ['endpointRequired', 'endpointMustBeURL'] },
|
|
{ path: 'graphEndpoint', rules: ['graphEndpointRequired', 'graphEndpointMustBeURL'] },
|
|
{ path: 'tokenEndpoint', rules: ['tokenEndpointRequired', 'tokenEndpointMustBeURL'] },
|
|
{ path: 'authEndpoint', rules: ['authEndpointRequired', 'authEndpointMustBeURL'] },
|
|
]
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
// Cannot pass this.model as a rootObject because it is undefined at that point, so had to use a workaround
|
|
fvExtraRules() {
|
|
return {
|
|
tenantIdRequired: this.modelFieldRequired('tenantId', 'authConfig.azuread.tenantId.label'),
|
|
applicationIdRequired: this.modelFieldRequired('applicationId', 'authConfig.azuread.applicationId.label'),
|
|
applicationSecretRequired: this.applicationSecretRequired(),
|
|
endpointRequired: this.modelFieldRequired('endpoint', 'authConfig.azuread.endpoint.label'),
|
|
endpointMustBeURL: this.modelFieldURL('endpoint'),
|
|
graphEndpointRequired: this.modelFieldRequired('graphEndpoint', 'authConfig.azuread.graphEndpoint.label'),
|
|
graphEndpointMustBeURL: this.modelFieldURL('graphEndpoint'),
|
|
tokenEndpointRequired: this.modelFieldRequired('tokenEndpoint', 'authConfig.azuread.tokenEndpoint.label'),
|
|
tokenEndpointMustBeURL: this.modelFieldURL('tokenEndpoint'),
|
|
authEndpointRequired: this.modelFieldRequired('authEndpoint', 'authConfig.azuread.authEndpoint.label'),
|
|
authEndpointMustBeURL: this.modelFieldURL('authEndpoint')
|
|
};
|
|
},
|
|
|
|
tArgs() {
|
|
return {
|
|
baseUrl: this.baseUrl,
|
|
provider: this.displayName,
|
|
username: this.principal.loginName || this.principal.name
|
|
};
|
|
},
|
|
|
|
replyUrl() {
|
|
return `${ this.serverUrl }/verify-auth-azure`;
|
|
},
|
|
|
|
tenantId() {
|
|
return this.model?.tenantId;
|
|
},
|
|
|
|
toSave() {
|
|
const applicationSecret = this.getNewApplicationSecret();
|
|
|
|
if (applicationSecret) {
|
|
this.model['applicationSecret'] = applicationSecret;
|
|
}
|
|
|
|
return {
|
|
config: {
|
|
...this.model,
|
|
enabled: true,
|
|
description: 'Enable AzureAD'
|
|
}
|
|
};
|
|
},
|
|
|
|
needsUpdate() {
|
|
return (
|
|
get(this.model, `annotations."${ AZURE_MIGRATED }"`) !== 'true'
|
|
);
|
|
},
|
|
|
|
modalConfig() {
|
|
return {
|
|
applyAction: this.updateEndpoint,
|
|
applyMode: 'update',
|
|
title: this.t('authConfig.azuread.updateEndpoint.modal.title'),
|
|
body: this.t('authConfig.azuread.updateEndpoint.modal.body', null, { raw: true })
|
|
};
|
|
},
|
|
editMemberConfig() {
|
|
return this.model.enabled && !this.isEnabling && !this.editConfig;
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
endpoint(value) {
|
|
this.setEndpoints(value);
|
|
},
|
|
|
|
tenantId() {
|
|
if (this.endpoint !== 'custom') {
|
|
this.setEndpoints(this.endpoint);
|
|
}
|
|
},
|
|
|
|
model: {
|
|
deep: true,
|
|
handler() {
|
|
this.model.accessMode = this.model.accessMode || 'unrestricted';
|
|
this.model.rancherUrl = this.model.rancherUrl || this.replyUrl;
|
|
|
|
if (this.model.applicationSecret) {
|
|
this['applicationSecret'] = this.model.applicationSecret;
|
|
}
|
|
}
|
|
},
|
|
|
|
},
|
|
|
|
methods: {
|
|
toggleGroupMembershipFilter(enabled) {
|
|
// reset the value of groupMembershipFilter when its filter gets disabled
|
|
if (!enabled) {
|
|
this.model.groupMembershipFilter = '';
|
|
}
|
|
},
|
|
|
|
setEndpoints(endpoint) {
|
|
if (this.editConfig || !this.model.enabled) {
|
|
const endpointType = this.oldEndpoint && endpoint !== 'custom' ? OLD_ENDPOINTS : ENDPOINT_MAPPING;
|
|
|
|
Object.keys(endpointType[endpoint]).forEach((key) => {
|
|
this.model[key] = endpointType[endpoint][key].replace(TENANT_ID_TOKEN, this.model.tenantId);
|
|
});
|
|
}
|
|
},
|
|
|
|
setInitialEndpoint(endpoint) {
|
|
const newEndpointKey = this.determineEndpointKeyType(ENDPOINT_MAPPING);
|
|
const oldEndpointKey = Object.keys(OLD_ENDPOINTS).find((key) => OLD_ENDPOINTS[key].graphEndpoint === endpoint);
|
|
|
|
if ( oldEndpointKey ) {
|
|
this.endpoint = this.determineEndpointKeyType(OLD_ENDPOINTS);
|
|
this.oldEndpoint = true;
|
|
} else {
|
|
this.endpoint = newEndpointKey;
|
|
}
|
|
},
|
|
|
|
determineEndpointKeyType(endpointTypes) {
|
|
let out = 'custom';
|
|
|
|
for ( const [endpointKey, endpointKeyValues] of Object.entries(endpointTypes) ) {
|
|
const mappedValues = Object.values(endpointKeyValues).map((endpoint) => endpoint.replace(TENANT_ID_TOKEN, this.model?.tenantId));
|
|
const valuesToCheck = Object.keys(endpointKeyValues).map((key) => this.value[key]);
|
|
|
|
if ( isEqual(mappedValues, valuesToCheck) ) {
|
|
out = endpointKey;
|
|
}
|
|
}
|
|
|
|
return out;
|
|
},
|
|
|
|
getNewApplicationSecret() {
|
|
const applicationSecretOrId =
|
|
this.model.applicationSecret || this.applicationSecret;
|
|
|
|
// The application secret comes back as an ID from steve API and this indicates
|
|
// that the current application secret isn't new
|
|
if (applicationSecretOrId.includes('cattle-global-data')) {
|
|
return null;
|
|
}
|
|
|
|
return applicationSecretOrId;
|
|
},
|
|
|
|
promptUpdate() {
|
|
this.$store.dispatch('management/promptModal', {
|
|
component: 'GenericPrompt',
|
|
componentProps: this.modalConfig
|
|
});
|
|
},
|
|
|
|
// update the authconfig to change the azure ad graph endpoint to the microsoft graph endpoint
|
|
// only relevant for setups upgrading to 2.6.6 with azuread auth already enabled
|
|
updateEndpoint(btnCB) {
|
|
if (this.needsUpdate) {
|
|
this.model
|
|
.doAction('upgrade')
|
|
.then(() => {
|
|
this.reloadModel();
|
|
this.$store.dispatch('growl/success', { message: 'Graph endpoint updated successfully.' });
|
|
btnCB(true);
|
|
})
|
|
.catch((err) => {
|
|
this.$store.dispatch('growl/fromError', {
|
|
title: 'Error updating graph endpoint',
|
|
err
|
|
});
|
|
btnCB(false);
|
|
});
|
|
}
|
|
},
|
|
modelFieldRequired(path, label) {
|
|
return () => {
|
|
return !this.model[path] ? `${ this.t('validation.required', { key: this.t(label) }) }` : undefined;
|
|
};
|
|
},
|
|
applicationSecretRequired() {
|
|
return () => {
|
|
return !this.editMemberConfig && !this.model.applicationSecret ? `${ this.t('validation.required', { key: this.t('authConfig.azuread.applicationSecret.label') }) }` : undefined;
|
|
};
|
|
},
|
|
modelFieldURL(path) {
|
|
return () => {
|
|
const rule = formRulesGenerator(this.$store.getters['i18n/t'], {}).url;
|
|
|
|
return rule(this.model[path]);
|
|
};
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Loading v-if="$fetchState.pending" />
|
|
<div v-else>
|
|
<CruResource
|
|
:done-route="doneRoute"
|
|
:mode="mode"
|
|
:resource="model"
|
|
:subtypes="[]"
|
|
:validation-passed="fvFormIsValid"
|
|
:finish-button-mode="model && model.enabled ? 'edit' : 'enable'"
|
|
:can-yaml="false"
|
|
:errors="errors"
|
|
:show-cancel="showCancel"
|
|
:cancel-event="true"
|
|
@error="e=>errors = e"
|
|
@finish="save"
|
|
@cancel="cancel"
|
|
>
|
|
<template v-if="editMemberConfig">
|
|
<AuthBanner
|
|
:t-args="tArgs"
|
|
:disable="disable"
|
|
:edit="goToEdit"
|
|
>
|
|
<template #rows>
|
|
<tr>
|
|
<td>{{ t(`authConfig.azuread.tenantId.label`) }}:</td>
|
|
<td>{{ model.tenantId }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{{ t(`authConfig.azuread.applicationId.label`) }}:</td>
|
|
<td>{{ model.applicationId }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{{ t(`authConfig.azuread.endpoint.label`) }}:</td>
|
|
<td>{{ model.endpoint }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{{ t(`authConfig.azuread.graphEndpoint.label`) }}:</td>
|
|
<td>{{ model.graphEndpoint }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{{ t(`authConfig.azuread.tokenEndpoint.label`) }}:</td>
|
|
<td>{{ model.tokenEndpoint }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{{ t(`authConfig.azuread.authEndpoint.label`) }}:</td>
|
|
<td>{{ model.authEndpoint }}</td>
|
|
</tr>
|
|
</template>
|
|
<template
|
|
v-if="needsUpdate"
|
|
#actions
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm role-secondary mr-10 update"
|
|
@click="promptUpdate"
|
|
>
|
|
{{ t('authConfig.azuread.updateEndpoint.button') }}
|
|
</button>
|
|
</template>
|
|
</AuthBanner>
|
|
|
|
<hr role="none">
|
|
|
|
<AllowedPrincipals
|
|
provider="azuread"
|
|
:auth-config="model"
|
|
:mode="mode"
|
|
/>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<AuthProviderWarningBanners
|
|
v-if="!model.enabled"
|
|
:t-args="tArgs"
|
|
/>
|
|
|
|
<InfoBox
|
|
v-if="!model.enabled"
|
|
id="reply-info"
|
|
class="mt-20 mb-20 p-10"
|
|
>
|
|
{{ t('authConfig.azuread.reply.info') }}
|
|
<br>
|
|
<label class="reply-url">{{ t('authConfig.azuread.reply.label') }} </label>
|
|
<CopyToClipboardText
|
|
:plain="true"
|
|
:aria-label="t('authConfig.azuread.reply.ariaLabel')"
|
|
:text="replyUrl"
|
|
/>
|
|
</InfoBox>
|
|
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
id="tenant-id"
|
|
v-model:value="model.tenantId"
|
|
:label="t('authConfig.azuread.tenantId.label')"
|
|
:mode="mode"
|
|
:required="true"
|
|
:rules="fvGetAndReportPathRules('tenantId')"
|
|
:tooltip="t('authConfig.azuread.tenantId.tooltip')"
|
|
:placeholder="t('authConfig.azuread.tenantId.placeholder')"
|
|
data-testid="input-azureAD-tenantId"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
id="application-id"
|
|
v-model:value="model.applicationId"
|
|
:label="t('authConfig.azuread.applicationId.label')"
|
|
:mode="mode"
|
|
:required="true"
|
|
:rules="fvGetAndReportPathRules('applicationId')"
|
|
:placeholder="t('authConfig.azuread.applicationId.placeholder')"
|
|
data-testid="input-azureAD-applcationId"
|
|
/>
|
|
</div>
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
id="application-secret"
|
|
v-model:value="model.applicationSecret"
|
|
type="password"
|
|
:label="t('authConfig.azuread.applicationSecret.label')"
|
|
:required="true"
|
|
:rules="fvGetAndReportPathRules('applicationSecret')"
|
|
:mode="mode"
|
|
data-testid="input-azureAD-applicationSecret"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-20">
|
|
<div class="col span-12">
|
|
<Checkbox
|
|
v-model:value="isGroupMembershipFilterEnabled"
|
|
class="mb-10 mr-10"
|
|
:mode="mode"
|
|
:label="t('authConfig.azuread.groupMembershipFilter.enable')"
|
|
:tooltip="t('authConfig.azuread.groupMembershipFilter.tooltip')"
|
|
data-testid="checkbox-azureAD-groupMembershipFilter"
|
|
@update:value="toggleGroupMembershipFilter"
|
|
/>
|
|
<div v-if="isGroupMembershipFilterEnabled">
|
|
<LabeledInput
|
|
v-model:value="model.groupMembershipFilter"
|
|
:label="t('authConfig.azuread.groupMembershipFilter.label')"
|
|
placeholder="e.g. (displayName eq 'group1') or (displayName eq 'group2')"
|
|
:mode="mode"
|
|
class="mb-10"
|
|
data-testid="input-azureAD-groupMembershipFilter"
|
|
/>
|
|
<a
|
|
:href="t('authConfig.azuread.groupMembershipFilter.externalHelpLink')"
|
|
target="_blank"
|
|
rel="noopener noreferrer nofollow"
|
|
>
|
|
{{ t('authConfig.azuread.groupMembershipFilter.externalHelp') }} <i class="icon icon-external-link" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<RadioGroup
|
|
v-model:value="endpoint"
|
|
class="mb-20"
|
|
:required="true"
|
|
:label="t('authConfig.azuread.endpoints.label')"
|
|
name="endpoints"
|
|
:options="['standard', 'china', 'custom']"
|
|
:mode="mode"
|
|
:labels="[t('authConfig.azuread.endpoints.standard'), t('authConfig.azuread.endpoints.china'), t('authConfig.azuread.endpoints.custom')]"
|
|
data-testid="endpoints-radio-input"
|
|
/>
|
|
<div v-if="endpoint === 'custom'">
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model:value="model.endpoint"
|
|
:label="t('authConfig.azuread.endpoint.label')"
|
|
:mode="mode"
|
|
:required="true"
|
|
:rules="fvGetAndReportPathRules('endpoint')"
|
|
data-testid="input-azureAD-endpoint"
|
|
/>
|
|
</div>
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model:value="model.graphEndpoint"
|
|
:label="t('authConfig.azuread.graphEndpoint.label')"
|
|
:required="true"
|
|
:rules="fvGetAndReportPathRules('graphEndpoint')"
|
|
:mode="mode"
|
|
data-testid="input-azureAD-graphEndpoint"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model:value="model.tokenEndpoint"
|
|
:label="t('authConfig.azuread.tokenEndpoint.label')"
|
|
:mode="mode"
|
|
:required="true"
|
|
:rules="fvGetAndReportPathRules('tokenEndpoint')"
|
|
data-testid="input-azureAD-tokenEndpoint"
|
|
/>
|
|
</div>
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model:value="model.authEndpoint"
|
|
:label="t('authConfig.azuread.authEndpoint.label')"
|
|
:required="true"
|
|
:rules="fvGetAndReportPathRules('authEndpoint')"
|
|
:mode="mode"
|
|
data-testid="input-azureAD-authEndpoint"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</CruResource>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
#reply-info {
|
|
flex-grow: 0;
|
|
}
|
|
|
|
.reply-url {
|
|
color: inherit;
|
|
font-weight: 700;
|
|
}
|
|
</style>
|