Merge remote-tracking branch 'origin/update-user-password-2' into api-keys

This commit is contained in:
Richard Cox 2021-01-25 14:14:00 +00:00
commit 2a117c8c10
38 changed files with 2068 additions and 284 deletions

View File

@ -75,9 +75,7 @@ BUTTON,
.checkbox-custom,
.radio-custom {
&:focus, &.focused {
outline: none;
box-shadow: 0 0 0 var(--outline-width) var(--outline);
background: var(--input-focus-bg) }
@include form-focus }
}
A {

View File

@ -155,4 +155,11 @@
}
}
}
}
@mixin form-focus {
// Focus for form like elements (not to be confused with basic :focus style)
outline: none;
box-shadow: 0 0 0 var(--outline-width) var(--outline);
background: var(--input-focus-bg)
}

View File

@ -4,6 +4,7 @@
width: 100%;
.link-container {
position: relative;
background-color: var(--input-bg);
border-radius: var(--border-radius);
border: solid 1px var(--input-border);
@ -25,6 +26,20 @@
> * {
opacity: .3;
}
.disabled-msg{
position:absolute;
color: var(--error);
z-index: z-index('hoverOverContent');
opacity: 1;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
display: flex;
justify-content: center;
align-items: flex-end;
}
}
&:hover:not(.disabled) {

View File

@ -138,49 +138,146 @@ account:
authConfig:
accessMode:
label: Configure who should be able to login and use {vendor}
unrestricted: "Allow any valid user"
restricted: "Allow members of clusters and projects, plus authorized users & groups"
required: "Restrict access to only the auhorized users & groups"
label: 'Configure who should be able to login and use {vendor}'
required: Restrict access to only the auhorized users & groups
restricted: 'Allow members of clusters and projects, plus authorized users & groups'
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>)."
stateBanner:
enabled: "{provider} is currently enabled."
disabled: "{provider} is currently disabled."
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>).'
github:
target:
label: Which version of GitHub do you want to use?
public: Public GitHub.com
private: A private installation of GitHub Enterprise
host:
label: GitHub Enterprise Host
placeholder: e.g. github.mycompany.example
clientId:
label: Client ID
clientSecret:
label: Client Secret
form:
app:
label: Application name
value: 'Anything you like, e.g. My {vendor}'
calllback:
label: Authorization callback URL
description:
label: Description
value: 'Optional, can be left blank'
homepage:
label: Homepage URL
instruction: 'Fill in the form with these values:'
prefix: |-
<li><a href="{baseUrl}/settings/developers" target="_blank" rel="noopener noreferrer nofollow">Click here</a> to go to GitHub application settings in a new window.</li>
<li>Click on the "OAuth Apps" tab.</li>
<li>Click the "New OAuth App" button.</li>
instruction: "Fill in the form with these values:"
app:
label: Application name
value: Anything you like, e.g. My {vendor}
homepage:
label: Homepage URL
description:
label: Description
value: Optional, can be left blank
calllback:
label: Authorization callback URL
suffix: |-
<li>Click "Register application"</li>
<li>Copy and paste the Client ID and Client Secret of your newly created OAuth app into the fields below</li>
host:
label: GitHub Enterprise Host
placeholder: e.g. github.mycompany.example
target:
label: Which version of GitHub do you want to use?
private: A private installation of GitHub Enterprise
public: Public GitHub.com
googleoauth:
adminEmail: Admin Email
domain: Domain
oauthCredentials:
label: OAuth Credentials
tip: The OAuth Credentials JSON can be found in the Google API developers console.
serviceAccountCredentials:
label: Service Account Credentials
tip: The Service Account Credentials JSON can be found in the service accounts section of the Google API developers console.
steps:
1:
title: 'Step One: For standard Google, click <a href="https://console.developers.google.com/apis/credentials" target="_blank" rel="noopener noreferrer nofollow">here</a> to go applications settings in a new window'
body: |-
<ul class="mt-0">
<li>Login to your account. Navigate to "APIs & Services" and then select "OAuth consent screen". </li>
<li>Authorized domains: Top private domain of {hostname} </li>
<li>Application homepage link: {serverUrl}</li>
<li>Under Scopes for Google APIs, enable "email", "profile", and "openid". </li>
<li>Click on "Save". </li>
</ul>
2:
title: 'Step Two: Navigate to the "Credentials" tab to create your OAuth client ID'
body: |-
<ul class="mt-0">
<li>Select the "Create Credentials" dropdown, and select "OAuth clientID", then select "Web application".</li>
<li>Authorized Javascript origins: {serverUrl}</li>
<li>Authorized redirect URIs: {serverUrl}/verify?test </li>
<li>Click "Create", and then click on the "Download JSON" button.</li>
<li>Upload the downloaded JSON file in the OAuth credentials box.</li>
</ul>
3:
title: 'Step Three: Create Service Account credentials'
body: |-
Follow <a href="https://rancher.com/docs/rancher/v2.x/en/admin-settings/authentication/google/#creating-service-account-credentials" target="_blank" rel="noopener noreferrer nofollow">this</a> guide to:<br/>
<ul class="mt-0">
<li> Create a service account.</li>
<li> Generate a key for the service account.</li>
<li> Add the service account as an OAuth client in your google domain.</li>
</ul>
ldap:
freeipa: Configure a FreeIPA server
activedirectory: Configure an Active Directory account
openldap: Configure an OpenLDAP server
defaultLoginDomain: Default Login Domain
cert: Certificate
disabledStatusBitmask: Disabled Status Bitmask
groupDNAttribute: Group DN Attribute
groupMemberMappingAttribute: Group Member Mapping Attribute
groupMemberUserAttribute: Group Member User Attribute
groupSearchBase:
label: Group Search Base
placeholder: 'ou=groups,dc=mycompany,dc=com'
hostname: Hostname/IP
loginAttribute: Login Attribute
nameAttribute: Name Attribute
nestedGroupMembership:
label: Nested Group Membership
options:
direct: Search only direct group memberships
nested: Search direct and nested group memberships
objectClass: Object Class
password: Password
port: Port
customizeSchema: Customize Schema
users: Users
groups: Groups
searchAttribute: Search Attribute
searchFilter: Search Filter
serverConnectionTimeout: Server Connection Timeout
serviceAccountDN: Service Account Distinguished Name
serviceAccountPassword: Service Account Password
serviceAccountInfo: Rancher needs a service account that has read-only access to all of the domains that will be able to login, so that we can determine what groups a user is a member of when they make a request with an API key.
starttls:
label: Start TLS
tip: Upgrades non-encrypted connections by wrapping with TLS during the connection process. Can not be used in conjunction with TLS.
tls: TLS
userEnabledAttribute: User Enabled Attribute
userMemberAttribute: User Member Attribute
userSearchBase:
label: User Search Base
placeholder: 'e.g. ou=users,dc=mycompany,dc=com'
username: Username
usernameAttribute: Username Attribute
saml:
UID: UID Field
adfs: Configure an AD FS account
api: Rancher API Host
cert: Certificate
displayName: Display Name Field
groups: Groups Field
key: Private Key
keycloak: Configure a Keycloak account
metadata: Metadata XML
okta: Configure an Okta account
ping: Configure a Ping account
shibboleth: Congiure a Shibboleth account
showLdap: Configure an OpenLDAP Server
userName: User Name Field
stateBanner:
disabled: '{provider} is currently disabled.'
enabled: '{provider} is currently enabled.'
testAndEnable: Test and Enable Authentication
@ -263,48 +360,45 @@ asyncButton:
backupRestoreOperator:
backupFilename: Backup Filename
deployment:
rancherNamespace: Rancher ResourceSet Namespace
storage:
tip: Configure a storage location where all backups are saved by default. You will have the option to override this with each backup, but will be limited to using an S3-compatible object store.
storageClass:
label: Storage Class
persistentVolume:
label: Persistent Volume
label: Default Storage Location
options:
none: No default storage location
s3: Use an S3-compatible object store
defaultStorageClass: 'Use the default storage class ({name})'
pickSC: Use an existing storage class
pickPV: Use an existing persistent volume
warning: This {type} does not have its reclaim policy set to "Retain". Your backups may be lost if the volume is changed or becomes unbound.
size: Size
prune:
label: Prune
tip: Delete the resources managed by Rancher that are not present in the backup. (Recommended)
encryption: Encryption
encryptionConfigName:
label: Encryption Config Secret
backuptip: Any secret in the <code>cattle-resource-system</code> namespace that has an <code>encryption-provider-config.yaml</code> key. <br/>The contents of this file are necessary to perform a restore from this backup, and are not stored by Rancher Backup.
restoretip: If the backup was performed with encryption enabled, a secret containing the same encryption-provider-config should be used during restore.
options:
none: Store the contents of the backup unencrypted
secret: Encrypt backups using an <a target="_blank" rel="noopener noreferrer nofollow" href="https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/#understanding-the-encryption-at-rest-configuration">Encryption Config Secret</a> (Recommended)
warning: The contents of this file are necessary to perform a restore from this backup, and are not stored by Rancher Backup.
deleteTimeout:
label: Delete Timeout
tip: Seconds to wait for a resource delete to succeed before removing finalizers to force deletion.
resourceSetName: Resource Set
schedule:
label: Schedule
placeholder: e.g. @midnight or 0 0 * * *
deployment:
rancherNamespace: Rancher ResourceSet Namespace
size: Size
storage:
label: Default Storage Location
options:
defaultStorageClass: 'Use the default storage class ({name})'
none: No default storage location
pickPV: Use an existing persistent volume
pickSC: Use an existing storage class
s3: Use an S3-compatible object store
persistentVolume:
label: Persistent Volume
storageClass:
label: Storage Class
tip: 'Configure a storage location where all backups are saved by default. You will have the option to override this with each backup, but will be limited to using an S3-compatible object store.'
warning: 'This {type} does not have its reclaim policy set to "Retain". Your backups may be lost if the volume is changed or becomes unbound.'
encryption: Encryption
encryptionConfigName:
backuptip: 'Any secret in the <code>cattle-resource-system</code> namespace that has an <code>encryption-provider-config.yaml</code> key. <br/>The contents of this file are necessary to perform a restore from this backup, and are not stored by Rancher Backup.'
label: Encryption Config Secret
options:
disabled: One-Time Backup
enabled: Recurring Backups
none: Store the contents of the backup unencrypted
secret: 'Encrypt backups using an <a target="_blank" rel="noopener noreferrer nofollow" href="https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/#understanding-the-encryption-at-rest-configuration">Encryption Config Secret</a> (Recommended)'
restoretip: 'If the backup was performed with encryption enabled, a secret containing the same encryption-provider-config should be used during restore.'
warning: 'The contents of this file are necessary to perform a restore from this backup, and are not stored by Rancher Backup.'
lastBackup: Last Backup
nextBackup: Next Backup
noResourceSet: You must define a ResourceSet in this namespace to create a backup CR.
prune:
label: Prune
tip: Delete the resources managed by Rancher that are not present in the backup. (Recommended)
resourceSetName: Resource Set
restoreFrom:
existing: An existing backup config
default: The default storage target
existing: An existing backup config
s3: An S3-compatible object store
retentionCount:
label: Retention Count
@ -314,24 +408,30 @@ backupRestoreOperator:
other { Files }
}
s3:
bucketName: Bucket Name
credentialSecretName: Credential Secret
endpoint: Endpoint
endpointCA: Endpoint CA
folder: Folder
insecureTLSSkipVerify: Skip TLS Verifications
region: Region
storageLocation: Storage Location
titles:
backupLocation: Backup Source
location: Storage Location
s3: S3
credentialSecretName: Credential Secret
storageLocation: Storage Location
endpoint: Endpoint
endpointCA: Endpoint CA
bucketName: Bucket Name
region: Region
folder: Folder
insecureTLSSkipVerify: Skip TLS Verifications
schedule:
label: Schedule
options:
disabled: One-Time Backup
enabled: Recurring Backups
placeholder: e.g. @midnight or 0 0 * * *
storageSource:
useDefault: Use the default storage location configured during installation
configureS3: Use an S3-compatible object store
useBackup: Use the s3 location specified on the Backup CR
useDefault: Use the default storage location configured during installation
targetBackup: Target Backup
noResourceSet: 'You must define a ResourceSet in this namespace to create a backup CR.'
catalog:
@ -807,6 +907,7 @@ istio:
jaeger:
label: Jaeger
description: Monitor and Troubleshoot microservices-based distributed systems.
disabled: '{app} is not installed'
cni: Enabled CNI
customOverlayFile:
label: Custom Overlay File
@ -843,7 +944,7 @@ logging:
noOutputsBanner: There are no cluster outputs in the selected namespace.
flow:
clusterOutputs:
doesntExistTooltip: This cluster output doesn't exist
doesntExistTooltip: This cluster output doesn't exist
label: Cluster Outputs
matches:
label: Matches
@ -1089,7 +1190,7 @@ monitoring:
className: Storage Class Name
existingClaim: Use Existing Claim
finalizers: PVC Finalizers
label: Persistent Storage for Grafana
label: Grafana Storage
mode: Access Mode
selector: Selector
size: Size
@ -1250,6 +1351,8 @@ namespace:
project:
label: Project
resources: Resources
enableAutoInjection: Enable Istio Auto Injection
disableAutoInjection: Disable Istio Auto Injection
namespaceFilter:
selected:
@ -1340,6 +1443,35 @@ prefs:
hideDesc:
label: Hide All Type Description Boxes
accountAndKeys:
title: Account and API Keys
account:
title: Account
name: Name
username: Username
change: Change Password
keys:
title: API Keys
changePassword:
title: Change Password
cancel: Cancel
keys: Delete all existing API keys
generatePassword: Generate a random password
newGeneratedPassword: Suggest a password
currentPassword: Current Password
userGen:
newPassword: New Password
confirmPassword: Confirm Password
randomGen:
copy: Copy to Clipboard
generated: Generated Password
errors:
missmatchedPassword: Passwords do not match
failedToChange: Failed to change password
failedDeleteKey: Failed to delete key
failedDeleteKeys: Failed to delete keys
principal:
loading: Loading&hellip;
error: Unable to fetch principal info
@ -1639,8 +1771,15 @@ servicesPage:
ports:
label: Ports
selectors:
helpText: "If no selector is created, manual endpoints must be made."
helpText: ""
label: Selectors
matchingPods:
matchesSome: |-
{matched, plural,
=0 {Matches 0 of {total, number} pods. If no selector is created, manual endpoints must be made.}
=1 {Matches 1 of {total, number} pods: "{sample}"}
other {Matches {matched, number} of {total, number} existing pods, including "{sample}"}
}
serviceTypes:
clusterIp:
abbrv: IP
@ -2592,6 +2731,9 @@ action:
view: View Config
viewInApi: View in API
viewYaml: View YAML
show: Show
hide: Hide
copy: Copy
unit:
sec: secs

View File

@ -98,7 +98,6 @@ export default {
watch: {
storageSource(neu) {
this.reclaimWarning = false;
switch (neu) {
case 'pickSC':
this.value.persistence.enabled = true;
@ -107,12 +106,16 @@ export default {
this.value.persistence.storageClass = this.defaultStorageClass?.id;
this.storageClass = this.defaultStorageClass;
}
if (this.storageClass?.reclaimPolicy === 'Delete') {
this.reclaimWarning = true;
}
delete this.value.persistence.volumeName;
break;
case 'pickPV':
this.value.persistence.enabled = true;
this.value.s3.enabled = false;
this.value.persistence.storageClass = '-';
this.reclaimWarning = false;
break;
case 's3':
this.value.persistence.enabled = false;

View File

@ -0,0 +1,106 @@
<script>
import { mapGetters } from 'vuex';
import ChangePassword from '@/components/form/ChangePassword';
import Card from '@/components/Card';
import AsyncButton from '@/components/AsyncButton';
export default {
components: {
Card, AsyncButton, ChangePassword
},
data() {
return { valid: false, password: '' };
},
computed: { ...mapGetters({ t: 'i18n/t' }) },
methods: {
show(show) {
if (show) {
this.$modal.show('password-modal');
} else {
this.$modal.hide('password-modal');
}
},
async submit(buttonCb) {
try {
await this.$refs.changePassword.submit();
this.show(false);
buttonCb(true);
} catch (err) {
buttonCb(false);
}
}
},
};
</script>
<template>
<modal
class="change-password-modal"
name="password-modal"
:width="500"
:height="445"
>
<Card class="prompt-password" :show-highlight-border="false">
<h4 slot="title" class="text-default-text">
{{ t("changePassword.title") }}
</h4>
<div slot="body">
<form @submit.prevent>
<ChangePassword ref="changePassword" v-model="password" @valid="valid = $event" />
</form>
</div>
<template #actions>
<!-- type reset is required by lastpass -->
<button class="btn role-secondary" type="reset" @click="show(false)">
{{ t("changePassword.cancel") }}
</button>
<AsyncButton
type="submit"
mode="apply"
class="btn bg-error ml-10"
:disabled="!valid"
value="LOGIN"
@click="submit"
/>
</template>
</Card>
</modal>
</template>
<style lang="scss" scoped>
.change-password-modal {
::v-deep .v--modal {
display: flex;
.card-wrap {
display: flex;
flex-direction: column;
.card-body {
flex: 1;
justify-content: start;
& > div {
flex: 1;
display: flex;
}
}
.card-actions {
display: flex;
justify-content: flex-end;
width: 100%;
}
}
}
}
.prompt-password {
flex: 1;
display: flex;
form {
flex: 1;
}
}
</style>

View File

@ -192,7 +192,7 @@ export default {
groups() {
const map = {};
const defaultGroup = 'Questions';
let weight = 1;
let weight = this.shownQuestions.length;
for ( const q of this.shownQuestions ) {
if ( q.group ) {
@ -202,7 +202,7 @@ export default {
map[normalized] = {
name: q.group || defaultGroup,
questions: [],
weight: weight++,
weight: weight--,
};
}

View File

@ -82,7 +82,7 @@ export const getters = {
bulkAction.enabled = state.tableSelected.length > 0 && actionEnabledForSomeSelected;
});
return out;
return out.sort((a, b) => (b.weight || 0) - (a.weight || 0));
},
options(state = stateSchema) {

View File

@ -0,0 +1,266 @@
<script>
import { mapGetters } from 'vuex';
import Banner from '@/components/Banner';
import Checkbox from '@/components/form/Checkbox';
import Password from '@/components/form/Password';
import { NORMAN } from '@/config/types';
export default {
components: {
Checkbox, Banner, Password
},
props: {
value: {
type: [String],
default: ''
},
},
async fetch() {
if (this.principal.provider === 'local' && !!this.principal.loginName) {
this.username = this.principal.loginName;
}
const users = await this.$store.dispatch('rancher/findAll', {
type: NORMAN.USER,
opt: { url: '/v3/users', filter: { me: true } }
});
if (users && users.length === 1) {
this.username = users[0].username;
}
},
data(ctx) {
return {
username: '',
errorMessages: [],
pCanShowMissmatchedPassword: false,
pIsRandomGenerated: false,
form: {
deleteKeys: false,
currentP: '',
newP: '',
genP: '',
confirmP: ''
},
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
isRandomGenerated: {
get() {
return this.pIsRandomGenerated;
},
set(isRandomGenerated) {
this.pIsRandomGenerated = isRandomGenerated;
this.errorMessages = [];
this.validate();
}
},
passwordGen: {
get() {
return this.form.genP;
},
set(p) {
this.form.genP = p;
this.validate();
}
},
passwordCurrent: {
get() {
return this.form.currentP;
},
set(p) {
this.form.currentP = p;
this.validate();
}
},
passwordNew: {
get() {
return this.form.newP;
},
set(p) {
this.form.newP = p;
this.validate();
}
},
passwordConfirm: {
get() {
return this.form.confirmP;
},
set(p) {
this.form.confirmP = p;
this.validate();
}
},
passwordConfirmBlurred: {
get() {
return this.pCanShowMissmatchedPassword;
},
set(p) {
this.pCanShowMissmatchedPassword = p;
this.validate();
}
},
password() {
return this.isRandomGenerated ? this.passwordGen : this.passwordNew;
},
principal() {
return this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, this.$store.getters['auth/principalId']) || {};
},
},
methods: {
passwordsMatch() {
const match = this.passwordNew === this.passwordConfirm;
this.errorMessages = this.passwordConfirmBlurred && !match ? [this.t('changePassword.errors.missmatchedPassword')] : [];
return match;
},
validate() {
this.$emit('valid', this.isRandomGenerated ? !!this.passwordCurrent : this.passwordsMatch() && !!this.passwordCurrent && this.passwordNew);
this.$emit('input', this.password);
},
async submit() {
await this.changePassword();
if (this.form.deleteKeys) {
await this.deleteKeys();
}
},
async changePassword() {
try {
await this.$store.dispatch('rancher/collectionAction', {
type: NORMAN.USER,
actionName: 'changepassword',
body: {
currentPassword: this.form.currentP,
newPassword: this.isRandomGenerated ? this.form.genP : this.form.newP
},
});
} catch (err) {
this.errorMessages = [err.message || this.t('changePassword.errors.failedToChange')];
throw err;
}
},
async deleteKeys() {
try {
const tokens = await this.$store.dispatch('rancher/findAll', {
type: NORMAN.TOKEN,
opt: {
// Ensure we have any new tokens since last fetched... and that we don't attempt to delete previously deleted tokens
force: true
}
});
await Promise.all(tokens.reduce((res, token) => {
if (!token.current) {
res.push(token.remove());
}
return res;
}, []));
} catch (err) {
if (err.message) {
this.errorMessages = [err.message];
} else if (err.length > 1) {
this.errorMessages = [this.t('changePassword.errors.failedDeleteKeys')];
} else {
this.errorMessages = [this.t('changePassword.errors.failedDeleteKey')];
}
throw err;
}
},
},
};
</script>
<template>
<div class="change-password">
<div class="form">
<div class="fields">
<Checkbox v-model="form.deleteKeys" :label="t('changePassword.keys')" class="mt-10" />
<input
id="username"
type="text"
name="username"
autocomplete="username"
:value="username"
tabindex="-1"
>
<input
id="password"
type="password"
name="password"
autocomplete="password"
:value="password"
tabindex="-1"
>
<Password
v-model="passwordCurrent"
class="mt-10"
:label="t('changePassword.currentPassword')"
></Password>
<Password
v-if="isRandomGenerated"
v-model="passwordGen"
class="mt-10"
:is-random="true"
:label="t('changePassword.randomGen.generated')"
/>
<div v-else class="userGen">
<Password
v-model="passwordNew"
class="mt-10"
:label="t('changePassword.userGen.newPassword')"
/>
<Password
v-model="passwordConfirm"
class="mt-10"
:label="t('changePassword.userGen.confirmPassword')"
@blur="passwordConfirmBlurred = true"
/>
</div>
</div>
<Checkbox v-model="isRandomGenerated" :label="t('changePassword.generatePassword')" class="mt-10 type" />
</div>
<div v-if="errorMessages && errorMessages.length" class="text-error">
<Banner v-for="(err, i) in errorMessages" :key="i" color="error" :label="err" class="mb-0" />
</div>
</div>
</template>
<style lang="scss" scoped>
.change-password {
display: flex;
flex-direction: column;
.form {
display: flex;
flex-direction: column;
.fields{
height: 215px;
#username, #password {
height: 0;
width: 0;
background-size: 0;
padding: 0;
border: none;
}
}
}
}
</style>

View File

@ -80,6 +80,7 @@ export default {
},
onBlur() {
this.$emit('blur');
this.onBlurLabeled();
},

View File

@ -0,0 +1,117 @@
<script>
import { mapGetters } from 'vuex';
import LabeledInput from '@/components/form/LabeledInput';
import { CHARSET, randomStr } from '@/utils/string';
export default {
components: { LabeledInput },
props: {
value: {
default: '',
type: String,
},
isRandom: {
default: false,
type: Boolean,
},
label: {
default: '',
type: String,
},
name: {
default: '',
type: String
},
autocomplete: {
type: String,
default: ''
}
},
data() {
return { reveal: false };
},
computed: {
...mapGetters({ t: 'i18n/t' }),
password: {
get() {
return this.value;
},
set(val) {
this.$emit('input', val);
}
},
attributes() {
const attributes = { };
if (this.name) {
attributes.id = this.name;
attributes.name = this.name;
}
if (this.autocomplete) {
attributes.autocomplete = this.autocomplete;
}
return attributes;
}
},
created() {
if (this.isRandom) {
this.generatePassword();
}
},
methods: {
generatePassword() {
this.password = randomStr(16, CHARSET.ALPHA_NUM);
},
show(reveal) {
this.reveal = reveal;
}
}
};
</script>
<template>
<div class="password">
<LabeledInput
v-model="password"
v-bind="attributes"
:type="isRandom || reveal ? 'text' : 'password'"
:readonly="isRandom"
:label="label"
:required="!isRandom"
:disabled="isRandom"
@blur="$emit('blur', $event)"
>
<template #suffix>
<div v-if="isRandom" class="addon">
<a href="#" @click.prevent.stop="$copyText(password)">{{ t('action.copy') }}</a>
</div>
<div v-else class="addon">
<a v-if="reveal" href="#" @click.prevent.stop="reveal = false">{{ t('action.hide') }}</a>
<a v-else href="#" @click.prevent.stop="reveal=true">{{ t('action.show') }}</a>
</div>
</template>
</LabeledInput>
<div v-if="isRandom" class="mt-10 genPassword">
<a href="#" @click.prevent.stop="generatePassword"><i class="icon icon-refresh" /> {{ t('changePassword.newGeneratedPassword') }}</a>
</div>
</div>
</template>
<style lang="scss" scoped>
.password {
display: flex;
flex-direction: column;
.labeled-input {
.addon {
padding-left: 12px;
min-width: 65px;
}
}
.genPassword {
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@ -170,7 +170,7 @@ export default {
position: relative;
}
.row {
&.row {
display: flex;
.radio-container {
margin-right: 10px;

View File

@ -20,7 +20,7 @@ export default {
addPrefix: {
type: Boolean,
default: true
default: false
},
suffix: {

View File

@ -81,22 +81,22 @@ export default {
<ClusterSwitcher v-if="isMultiCluster && currentProduct && currentProduct.showClusterSwitcher" />
</div>
<div class="user">
<div class="user" tabindex="0" @blur="showMenu(false)" @click="showMenu(true)" @focus.capture="showMenu(true)">
<v-popover
ref="popover"
placement="bottom"
placement="bottom-end"
offset="-10"
trigger="manual"
:delay="{show: 0, hide: 200}"
:delay="{show: 0, hide: 0}"
:popper-options="{modifiers: { flip: { enabled: false } } }"
:container="false"
>
<div class="text-right" @mouseover="showMenu(true)" @click="showMenu(true)">
<div class="user-image text-right hand">
<img v-if="principal && principal.avatarSrc" :src="principal.avatarSrc" :class="{'avatar-round': principal.roundAvatar}" width="40" height="40" />
<i v-else class="icon icon-user icon-3x avatar" />
</div>
<template slot="popover">
<ul class="list-unstyled dropdown" @mouseleave="showMenu(false)">
<ul class="list-unstyled dropdown" @click.stop="showMenu(false)">
<li v-if="authEnabled" class="user-info">
<div class="user-name">
<i class="icon icon-lg icon-user" /> {{ principal.loginName }}
@ -105,15 +105,15 @@ export default {
{{ principal.name }}
</div>
</li>
<div @click="showMenu(false)">
<nuxt-link tag="li" :to="{name: 'account'}" class="hand">
<a>API Keys</a>
</nuxt-link>
<div>
<nuxt-link tag="li" :to="{name: 'prefs'}" class="hand">
<a>Preferences <i class="icon icon-fw icon-gear" /></a>
</nuxt-link>
<nuxt-link tag="li" :to="{name: 'account'}" class="hand">
<a>Account &amp; API Keys <i class="icon icon-fw icon-user" /></a>
</nuxt-link>
<nuxt-link v-if="authEnabled" tag="li" :to="{name: 'auth-logout'}" class="pt-5 pb-5 hand">
<a>Log Out <i class="icon icon-fw icon-close" /></a>
<a @blur="showMenu(false)">Log Out <i class="icon icon-fw icon-close" /></a>
</nuxt-link>
</div>
</ul>
@ -236,6 +236,18 @@ export default {
}
> .user {
outline: none;
&:focus {
.v-popover {
::v-deep .trigger {
.user-image > * {
@include form-focus
}
}
}
}
grid-area: user;
background-color: var(--header-bg);
padding: 5px;

View File

@ -68,6 +68,8 @@ export const RBAC = { PRODUCT: 'management.cattle.io/ui-product' };
export const RKE = { EXTERNAL_IP: 'rke.cattle.io/external-ip' };
export const ISTIO = { AUTO_INJECTION: 'istio-injection' };
const CATTLE_REGEX = /cattle\.io\//;
export const LABELS_TO_IGNORE_REGEX = [

View File

@ -42,6 +42,15 @@ export function init(store) {
});
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/github`, 'auth/github');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/openldap`, 'auth/ldap/index');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/freeipa`, 'auth/ldap/index');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/activedirectory`, 'auth/ldap/index');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/ping`, 'auth/saml');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/shibboleth`, 'auth/saml');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/okta`, 'auth/saml');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/keycloak`, 'auth/saml');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/adfs`, 'auth/saml');
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/googleoauth`, 'auth/googleoauth');
basicType([
'config',

View File

@ -27,18 +27,32 @@ export function init(store) {
]);
headers(BACKUP_RESTORE.BACKUP, [
STATE,
'Status',
{ ...STATE, value: 'Status' },
NAME_HEADER,
'Location',
'Type',
'Latest-Backup',
AGE,
{
name: 'backupFilename',
labelKey: 'backupRestoreOperator.backupFilename',
value: 'status.filename'
},
{
name: 'nextBackup',
labelKey: 'backupRestoreOperator.nextBackup',
value: 'status.nextSnapshotAt',
formatter: 'Date'
},
{
name: 'nextBackup',
labelKey: 'backupRestoreOperator.lastBackup',
value: 'status.lastSnapshotTs',
formatter: 'Date'
}
]);
headers(BACKUP_RESTORE.RESTORE, [
STATE,
'Status',
{ ...STATE, value: 'Status' },
NAME_HEADER,
'Backup-Source',
'Backup-File',

View File

@ -14,6 +14,8 @@ export const STEVE = {
export const NORMAN = {
AUTH_CONFIG: 'authconfig',
PRINCIPAL: 'principal',
USER: 'user',
TOKEN: 'token',
};
// Public (via Norman)
@ -126,6 +128,7 @@ export const MANAGEMENT = {
SETTING: 'management.cattle.io.setting',
TOKEN: 'management.cattle.io.token',
USER: 'management.cattle.io.user',
TOKEN: 'management.cattle.io.token',
};
export const CAPI = {

View File

@ -10,7 +10,7 @@ import AsyncButton from '@/components/AsyncButton';
import CopyToClipboardText from '@/components/CopyToClipboardText.vue';
import AllowedPrincipals from '@/components/auth/AllowedPrincipals';
import { NORMAN, MANAGEMENT } from '@/config/types';
import { addObject, findBy } from '@/utils/array';
import { findBy } from '@/utils/array';
const NAME = 'github';
@ -105,6 +105,14 @@ export default {
AUTH_CONFIG() {
return MANAGEMENT.AUTH_CONFIG;
},
toSave() {
return {
enabled: true,
githubConfig: this.model,
description: 'Enable GitHub',
};
}
},
watch: {
@ -113,18 +121,6 @@ export default {
},
methods: {
async reloadModel() {
this.originalModel = await this.$store.dispatch('rancher/find', {
type: NORMAN.AUTH_CONFIG,
id: NAME,
opt: { url: `/v3/${ NORMAN.AUTH_CONFIG }/${ NAME }`, force: true }
});
this.model = await this.$store.dispatch(`rancher/clone`, { resource: this.originalModel });
return this.model;
},
updateHost() {
const match = this.targetUrl.match(/^(((https?):)?\/\/)?([^/]+)(\/.*)?$/);
@ -138,67 +134,6 @@ export default {
this.model.hostname = match[4] || 'github.com';
}
},
async save(btnCb) {
this.errors = [];
const wasEnabled = this.model.enabled;
try {
if ( !wasEnabled ) {
const code = await this.$store.dispatch('auth/test', { provider: NAME, body: this.model });
this.model.enabled = true;
await this.model.doAction('testAndApply', {
code,
enabled: true,
githubConfig: this.model,
description: 'Enable GitHub',
});
// Reload principals to get the new ones from GitHub
this.principals = await this.$store.dispatch('rancher/findAll', {
type: NORMAN.PRINCIPAL,
opt: { url: '/v3/principals', force: true }
});
this.model.accessMode = 'restricted';
this.model.allowedPrincipalIds = this.model.allowedPrincipalIds || [];
if ( this.me && !this.model.allowedPrincipalIds.includes(this.me.id) ) {
addObject(this.model.allowedPrincipalIds, this.me.id);
}
}
await this.model.save();
await this.reloadModel();
btnCb(true);
if ( wasEnabled ) {
this.done();
}
} catch (err) {
this.errors = [err];
btnCb(false);
}
},
async disable(btnCb) {
try {
const clone = await this.$store.dispatch(`rancher/clone`, { resource: this.model });
clone.enabled = false;
await clone.save();
await this.reloadModel();
btnCb(true);
} catch (err) {
this.errors = [err];
btnCb(false);
}
}
},
};
</script>
@ -219,7 +154,7 @@ export default {
@finish="save"
@cancel="done"
>
<template v-if="model.enabled">
<template v-if="model.enabled && !isSaving">
<Banner color="success clearfix">
<div class="pull-left mt-10">
{{ t('authConfig.stateBanner.enabled', tArgs) }}

180
edit/auth/googleoauth.vue Normal file
View File

@ -0,0 +1,180 @@
<script>
import Loading from '@/components/Loading';
import CreateEditView from '@/mixins/create-edit-view';
import Auth from '@/mixins/auth';
import CruResource from '@/components/CruResource';
import InfoBox from '@/components/InfoBox';
import Checkbox from '@/components/form/Checkbox';
import LabeledInput from '@/components/form/LabeledInput';
import Banner from '@/components/Banner';
import AsyncButton from '@/components/AsyncButton';
import AllowedPrincipals from '@/components/auth/AllowedPrincipals';
import FileSelector from '@/components/form/FileSelector';
const NAME = 'googleoauth';
export default {
components: {
Loading,
CruResource,
InfoBox,
LabeledInput,
Banner,
Checkbox,
AllowedPrincipals,
AsyncButton,
FileSelector
},
mixins: [CreateEditView, Auth],
data() {
return {
model: null,
serverSetting: null,
errors: null,
};
},
computed: {
tArgs() {
let hostname = '';
if (process.client) {
hostname = window.location.hostname;
}
return {
hostname,
serverUrl: this.serverUrl,
provider: this.displayName,
username: this.principal.loginName || this.principal.name,
};
},
NAME() {
return NAME;
},
toSave() {
return {
enabled: true,
googleOauthConfig: this.model,
description: 'Enable Google OAuth',
};
}
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<CruResource
:done-route="doneRoute"
:mode="mode"
:resource="model"
:subtypes="[]"
:validation-passed="true"
:finish-button-mode="model.enabled ? 'edit' : 'enable'"
:can-yaml="false"
:errors="errors"
@error="e=>errors = e"
@finish="save"
@cancel="done"
>
<template v-if="model.enabled && !isSaving">
<Banner color="success clearfix">
<div class="pull-left mt-10">
{{ t('authConfig.stateBanner.enabled', tArgs) }}
</div>
<div class="pull-right">
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
</div>
</Banner>
<div>{{ t(`authConfig.${NAME}.adminEmail`) }}: {{ model.adminEmail }}</div>
<div>{{ t(`authConfig.${NAME}.domain`) }}: {{ model.hostname }}</div>
<hr />
<AllowedPrincipals provider="googleoauth" :auth-config="model" :mode="mode" />
</template>
<template v-else>
<Banner :label="t('authConfig.stateBanner.disabled', tArgs)" color="warning" />
<div :style="{'align-items':'center'}" class="row mb-20">
<div class="col span-5">
<LabeledInput
v-model="model.adminEmail"
:label="t(`authConfig.${NAME}.adminEmail`)"
:mode="mode"
required
/>
</div>
<div class="col span-5">
<LabeledInput
v-model="model.hostname"
:label="t(`authConfig.${NAME}.domain`)"
:mode="mode"
required
/>
</div>
<div class="col span-2">
<Checkbox v-model="model.nestedGroupMembershipEnabled" :mode="mode" :label="t('authConfig.ldap.nestedGroupMembership.label')" />
</div>
</div>
<InfoBox class=" mt-20 mb-20 p-10">
<h3 v-html="t('authConfig.googleoauth.steps.1.title', tArgs, true)" />
<div v-html="t('authConfig.googleoauth.steps.1.body', tArgs, true)" />
</InfoBox>
<InfoBox class="mb-20 p-10">
<div class="row">
<h3 v-html="t('authConfig.googleoauth.steps.2.title', tArgs, true)" />
</div>
<div class="row">
<div class="col span-6" v-html="t('authConfig.googleoauth.steps.2.body', tArgs, true)" />
<div class="col span-6">
<LabeledInput
v-model="model.oauthCredential"
:label="t(`authConfig.googleoauth.oauthCredentials.label`)"
:mode="mode"
required
type="multiline"
:tooltip="t(`authConfig.googleoauth.oauthCredentials.tip`)"
:hover-tooltip="true"
/>
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'oauthCredential', $event)" />
</div>
</div>
</InfoBox>
<InfoBox class="mb-20 p-10">
<div class="row">
<h3 v-html="t('authConfig.googleoauth.steps.3.title', tArgs, true)" />
</div>
<div class="row">
<div class="col span-6" v-html="t('authConfig.googleoauth.steps.3.body', tArgs, true)" />
<div class="col span-6">
<LabeledInput
v-model="model.serviceAccountCredential"
:label="t(`authConfig.googleoauth.serviceAccountCredentials.label`)"
:mode="mode"
required
type="multiline"
:tooltip="t(`authConfig.googleoauth.serviceAccountCredentials.tip`)"
:hover-tooltip="true"
/>
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'serviceAccountCredential', $event)" />
</div>
</div>
</InfoBox>
<div v-if="!model.enabled" class="row">
<div class="col span-12">
<Banner color="info" v-html="t('authConfig.associatedWarning', tArgs, true)" />
</div>
</div>
</template>
</CruResource>
</div>
</template>

205
edit/auth/ldap/config.vue Normal file
View File

@ -0,0 +1,205 @@
<script>
import RadioGroup from '@/components/form/RadioGroup';
import LabeledInput from '@/components/form/LabeledInput';
import Checkbox from '@/components/form/Checkbox';
import UnitInput from '@/components/form/UnitInput';
import Banner from '@/components/Banner';
import FileSelector from '@/components/form/FileSelector';
export default {
components: {
RadioGroup,
LabeledInput,
Banner,
Checkbox,
UnitInput,
FileSelector
},
props: {
value: {
type: Object,
required: true
},
mode: {
type: String,
default: 'edit'
},
type: {
type: String,
required: true
}
},
data() {
if (!this.value.servers) {
this.value.servers = [];
}
return {
model: this.value,
hostname: this.value.servers[0],
serverSetting: null,
};
},
watch: {
hostname(neu, old) {
this.value.servers[0] = neu;
},
'model.starttls'(neu) {
if (neu) {
this.model.tls = false;
}
},
'model.tls'(neu) {
if (neu) {
this.model.starttls = false;
}
}
}
};
</script>
<template>
<div @input="$emit('input', model)">
<template>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="hostname" required :mode="mode" :label="t('authConfig.ldap.hostname')" />
</div>
<div class="col span-5">
<LabeledInput v-model="model.port" required :mode="mode" :label="t('authConfig.ldap.port')" />
</div>
<div class="col span-1">
<Checkbox v-model="model.tls" :mode="mode" class="full-height" :label="t('authConfig.ldap.tls')" />
</div>
</div>
<div v-if="model.tls || model.starttls" class="row mb-20">
<div class="col span-12">
<LabeledInput v-model="model.certificate" required type="multiline" :mode="mode" :label="t('authConfig.ldap.cert')" />
<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">
<UnitInput v-model="model.connectionTimeout" required :mode="mode" :label="t('authConfig.ldap.serverConnectionTimeout')" suffix="milliseconds" />
</div>
<div v-if="type==='openldap'" class="col span-6">
<Checkbox v-model="model.starttls" :tooltip="t('authConfig.ldap.starttls.tip')" :mode="mode" class="full-height" :label="t('authConfig.ldap.starttls.label')" />
</div>
<div v-if="type==='activedirectory'" class="col span-6">
<LabeledInput v-model="model.defaultLoginDomain" required :mode="mode" :label="t('authConfig.ldap.defaultLoginDomain')" />
</div>
</div>
<Banner color="info" :label="t('authConfig.ldap.serviceAccountInfo')" />
<div class="row mb-20">
<div v-if="type==='activedirectory'" class="col span-6">
<LabeledInput v-model="model.serviceAccountUsername" required :mode="mode" :label="t('authConfig.ldap.serviceAccountDN')" />
</div>
<div v-else class="col span-6">
<LabeledInput v-model="model.serviceAccountDistinguishedName" required :mode="mode" :label="t('authConfig.ldap.serviceAccountDN')" />
</div>
<div class="col span-6">
<LabeledInput v-model="model.serviceAccountPassword" required type="password" :mode="mode" :label="t('authConfig.ldap.serviceAccountPassword')" />
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="model.userSearchBase" required :mode="mode" :label="t('authConfig.ldap.userSearchBase.label')" :placeholder="t('authConfig.ldap.userSearchBase.placeholder')" />
</div>
<div class="col span-6">
<LabeledInput v-model="model.groupSearchBase" type="password" :mode="mode" :placeholder="t('authConfig.ldap.groupSearchBase.placeholder')" :label="t('authConfig.ldap.groupSearchBase.label')" />
</div>
</div>
<div class="row">
<h3> {{ t('authConfig.ldap.customizeSchema') }}</h3>
</div>
<div class="row">
<div class="col span-6">
<h4>{{ t('authConfig.ldap.users') }}</h4>
</div>
<div class="col span-6">
<h4>{{ t('authConfig.ldap.groups') }}</h4>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="model.userObjectClass" :mode="mode" :label="t('authConfig.ldap.objectClass')" />
</div>
<div class="col span-6">
<LabeledInput v-model="model.groupObjectClass" :mode="mode" :label="t('authConfig.ldap.objectClass')" />
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="model.userNameAttribute" :mode="mode" :label="t('authConfig.ldap.usernameAttribute')" />
</div>
<div class="col span-6">
<LabeledInput v-model="model.groupNameAttribute" :mode="mode" :label="t('authConfig.ldap.nameAttribute')" />
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="model.userLoginAttribute" :mode="mode" :label="t('authConfig.ldap.loginAttribute')" />
</div>
<div class="col span-6">
<LabeledInput v-model="model.groupMemberUserAttribute" :mode="mode" :label="t('authConfig.ldap.groupMemberUserAttribute')" />
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="model.userMemberAttribute" :mode="mode" :label="t('authConfig.ldap.userMemberAttribute')" />
</div>
<div class="col span-6">
<LabeledInput v-model="model.groupSearchAttribute" :mode="mode" :label="t('authConfig.ldap.searchAttribute')" />
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="model.userSearchAttribute" :mode="mode" :label="t('authConfig.ldap.searchAttribute')" />
</div>
<div class="col span-6">
<LabeledInput v-model="model.groupSearchFilter" :mode="mode" :label="t('authConfig.ldap.searchFilter')" />
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="model.userSearchFilter" :mode="mode" :label="t('authConfig.ldap.searchFilter')" />
</div>
<div class="col span-6">
<LabeledInput v-model="model.groupMemberMappingAttribute" :mode="mode" :label="t('authConfig.ldap.groupMemberMappingAttribute')" />
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="model.userEnabledAttribute" :mode="mode" :label="t('authConfig.ldap.userEnabledAttribute')" />
</div>
<div class="col span-6">
<LabeledInput v-model="model.groupDNAttribute" :mode="mode" :label="t('authConfig.ldap.groupDNAttribute')" />
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="model.disabledStatusBitmask" :mode="mode" :label="t('authConfig.ldap.disabledStatusBitmask')" />
</div>
<div class=" col span-6">
<RadioGroup
v-model="model.nestedGroupMembershipEnabled"
:mode="mode"
name="nested"
class="full-height"
:options="[true, false]"
:labels="[t('authConfig.ldap.nestedGroupMembership.options.nested'), t('authConfig.ldap.nestedGroupMembership.options.direct')]"
/>
</div>
</div>
</template>
</div>
</template>

139
edit/auth/ldap/index.vue Normal file
View File

@ -0,0 +1,139 @@
<script>
import Loading from '@/components/Loading';
import CreateEditView from '@/mixins/create-edit-view';
import Auth from '@/mixins/auth';
import CruResource from '@/components/CruResource';
import LabeledInput from '@/components/form/LabeledInput';
import Banner from '@/components/Banner';
import AllowedPrincipals from '@/components/auth/AllowedPrincipals';
import AsyncButton from '@/components/AsyncButton';
import config from '@/edit/auth/ldap/config';
const AUTH_TYPE = 'ldap';
export default {
components: {
Loading,
CruResource,
LabeledInput,
Banner,
AllowedPrincipals,
AsyncButton,
config
},
mixins: [CreateEditView, Auth],
data() {
return {
model: null,
serverSetting: null,
errors: null,
username: null,
password: null
};
},
computed: {
tArgs() {
return {
provider: this.displayName,
username: this.principal.loginName || this.principal.name,
};
},
AUTH_TYPE() {
return AUTH_TYPE;
},
toSave() {
let out = {
enabled: true,
ldapConfig: this.model,
username: this.username,
password: this.password
};
if (this.NAME === 'activedirectory') {
out = {
enabled: true,
activeDirectoryConfig: this.model,
username: this.username,
password: this.password
};
}
return out;
}
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<CruResource
:done-route="doneRoute"
:mode="mode"
:resource="model"
:subtypes="[]"
:validation-passed="true"
:finish-button-mode="model.enabled ? 'edit' : 'enable'"
:can-yaml="false"
:errors="errors"
@error="e=>errors = e"
@finish="save"
@cancel="done"
>
<template v-if="model.enabled && !isSaving">
<Banner color="success clearfix">
<div class="pull-left mt-10">
{{ t('authConfig.stateBanner.enabled', tArgs) }}
</div>
<div class="pull-right">
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
</div>
</Banner>
<div>Server: {{ serverUrl }}</div>
<div>Client ID: {{ model.serviceAccountDistinguishedName || model.serviceAccountUsername }}</div>
<hr />
<AllowedPrincipals :provider="NAME" :auth-config="model" :mode="mode" />
</template>
<template v-else>
<h3>{{ t(`authConfig.ldap.${NAME}`) }}</h3>
<config v-model="model" :type="NAME" :mode="mode" />
<h4>{{ t('authConfig.testAndEnable') }}</h4>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model="username"
:label="t(`authConfig.${AUTH_TYPE}.username`)"
:mode="mode"
required
/>
</div>
<div class="col span-6">
<LabeledInput
v-model="password"
type="password"
:label="t(`authConfig.${AUTH_TYPE}.password`)"
:mode="mode"
required
/>
</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>

195
edit/auth/saml.vue Normal file
View File

@ -0,0 +1,195 @@
<script>
import Loading from '@/components/Loading';
import CreateEditView from '@/mixins/create-edit-view';
import Auth from '@/mixins/auth';
import CruResource from '@/components/CruResource';
import LabeledInput from '@/components/form/LabeledInput';
import Checkbox from '@/components/form/Checkbox';
import Banner from '@/components/Banner';
import AllowedPrincipals from '@/components/auth/AllowedPrincipals';
import AsyncButton from '@/components/AsyncButton';
import FileSelector from '@/components/form/FileSelector';
import config from '@/edit/auth/ldap/config';
const AUTH_TYPE = 'ldap';
export default {
components: {
Loading,
CruResource,
LabeledInput,
Banner,
AllowedPrincipals,
Checkbox,
FileSelector,
AsyncButton,
config
},
mixins: [CreateEditView, Auth],
data() {
return {
model: null,
errors: null,
serverSetting: null,
showLdap: false
};
},
computed: {
baseUrl() {
return `${ this.model.tls ? 'https://' : 'http://' }${ this.model.hostname }`;
},
tArgs() {
return {
baseUrl: this.serverSetting,
provider: this.displayName,
username: this.principal.loginName || this.principal.name,
};
},
AUTH_TYPE() {
return AUTH_TYPE;
},
toSave() {
return { enabled: true, ...this.model };
}
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<CruResource
:done-route="doneRoute"
:mode="mode"
:resource="model"
:subtypes="[]"
:validation-passed="true"
:finish-button-mode="model.enabled ? 'edit' : 'enable'"
:can-yaml="false"
:errors="errors"
@error="e=>errors = e"
@finish="save"
@cancel="done"
>
<template v-if="model.enabled && !isSaving">
<Banner color="success clearfix">
<div class="pull-left mt-10">
{{ t('authConfig.stateBanner.enabled', tArgs) }}
</div>
<div class="pull-right">
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
</div>
</Banner>
<div>Server: {{ baseUrl }}</div>
<div>Display Name: {{ model.displayNameField }}</div>
<hr />
<AllowedPrincipals provider="github" :auth-config="model" :mode="mode" />
</template>
<template v-else>
<h3>{{ t(`authConfig.saml.${NAME}`) }}</h3>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model="model.displayNameField"
:label="t(`authConfig.saml.displayName`)"
:mode="mode"
required
/>
</div>
<div class="col span-6">
<LabeledInput
v-model="model.userNameField"
:label="t(`authConfig.saml.userName`)"
:mode="mode"
required
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model="model.uidField"
:label="t(`authConfig.saml.UID`)"
:mode="mode"
required
/>
</div>
<div class="col span-6">
<LabeledInput
v-model="model.groupsField"
:label="t(`authConfig.saml.groups`)"
:mode="mode"
required
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model="model.rancherApiHost"
:label="t(`authConfig.saml.api`)"
:mode="mode"
required
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-4">
<LabeledInput
v-model="model.spKey"
:label="t(`authConfig.saml.key`)"
:mode="mode"
required
type="multiline"
/>
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'spKey', $event)" />
</div>
<div class="col span-4">
<LabeledInput
v-model="model.spCert"
:label="t(`authConfig.saml.cert`)"
:mode="mode"
required
type="multiline"
/>
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'spCert', $event)" />
</div>
<div class="col span-4">
<LabeledInput
v-model="model.idpMetadataContent"
:label="t(`authConfig.saml.metadata`)"
:mode="mode"
required
type="multiline"
/>
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'idpMetadataContent', $event)" />
</div>
</div>
<div v-if="NAME === 'shibboleth'">
<div class="row">
<Checkbox v-model="showLdap" :mode="mode" :label="t('authConfig.saml.showLdap')" />
</div>
<div class="row mt-20 mb-20">
<config v-if="showLdap" v-model="model.openLdapConfig" :type="NAME" :mode="mode" />
</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>

View File

@ -173,26 +173,26 @@ export default {
</template>
<style lang="scss" scoped>
.responder {
&, .target {
width: 100%;
}
.send-to {
margin-left: -35px;
}
.send-to {
margin-left: -35px;
}
.responder {
&, .target {
width: 100%;
}
.unlabeled-select ::v-deep {
.unlabeled-select ::v-deep {
height: $input-height;
}
.target ::v-deep {
& .input-container {
height: $input-height;
}
.target ::v-deep {
& .input-container {
height: $input-height;
}
& .unlabeled-select {
min-width: 35%;
}
& .unlabeled-select {
min-width: 35%;
}
}
}
</style>

View File

@ -171,9 +171,8 @@ export default {
<template>
<NameNsDescription :mode="mode" :value="value" :namespaced="false" @change="name=value.metadata.name" />
<template v-if="!!resourceSet">
<div v-if="!isView || setSchedule" class="bordered-section">
<div class="bordered-section">
<RadioGroup
v-if="!isView"
v-model="setSchedule"
:mode="mode"
:label="t('backupRestoreOperator.schedule.label')"
@ -183,7 +182,7 @@ export default {
/>
<div v-if="setSchedule" class="row mt-10 mb-10">
<div class="col span-6">
<LabeledInput v-model="value.spec.schedule" :mode="mode" :label="t('backupRestoreOperator.schedule.label')" :placeholder="t('backupRestoreOperator.schedule.placeholder')" />
<LabeledInput v-model="value.spec.schedule" type="cron" :mode="mode" :label="t('backupRestoreOperator.schedule.label')" :placeholder="t('backupRestoreOperator.schedule.placeholder')" />
</div>
<div class="col span-6">
<UnitInput v-model="value.spec.retentionCount" :suffix="t('backupRestoreOperator.retentionCount.units', {count: value.spec.retentionCount || 0})" :mode="mode" :label="t('backupRestoreOperator.retentionCount.label')" />
@ -191,11 +190,10 @@ export default {
</div>
</div>
<div v-if="!isView || useEncryption" class="bordered-section">
<div class="bordered-section">
<div class="row">
<div class="col span-12">
<RadioGroup
v-if="!isView"
v-model="useEncryption"
name="useEncryption"
:label="t('backupRestoreOperator.encryption')"

View File

@ -1,5 +1,6 @@
<script>
import { isEmpty } from 'lodash';
import throttle from 'lodash/throttle';
import ArrayList from '@/components/form/ArrayList';
import CreateEditView from '@/mixins/create-edit-view';
import KeyValue from '@/components/form/KeyValue';
@ -16,15 +17,17 @@ import CruResource from '@/components/CruResource';
import Banner from '@/components/Banner';
import Labels from '@/components/form/Labels';
import { clone } from '@/utils/object';
import { POD } from '@/config/types';
import { matching } from '@/utils/selector';
const SESSION_AFFINITY_ACTION_VALUES = {
NONE: 'None',
CLIENTIP: 'ClientIP'
CLIENTIP: 'ClientIP',
};
const SESSION_AFFINITY_ACTION_LABELS = {
NONE: 'servicesPage.affinity.actionLabels.none',
CLIENTIP: 'servicesPage.affinity.actionLabels.clientIp'
CLIENTIP: 'servicesPage.affinity.actionLabels.clientIp',
};
const SESSION_STICKY_TIME_DEFAULT = 10800;
@ -45,22 +48,36 @@ export default {
ServicePorts,
Tab,
Tabbed,
UnitInput
UnitInput,
},
mixins: [CreateEditView],
fetch() {
return this.loadPods();
},
data() {
if (!this?.value?.spec?.type) {
if (!this.value?.spec) {
this.$set(this.value, 'spec', {
ports: [],
sessionAffinity: 'None'
sessionAffinity: 'None',
});
}
}
const matchingPods = {
matched: 0,
matches: [],
none: true,
sample: null,
total: 0,
};
return {
matchingPods,
allPods: [],
defaultServiceTypes: DEFAULT_SERVICE_TYPES,
saving: false,
sessionAffinityActionLabels: Object.values(SESSION_AFFINITY_ACTION_LABELS)
@ -68,7 +85,7 @@ export default {
.map(ucFirst),
sessionAffinityActionOptions: Object.values(
SESSION_AFFINITY_ACTION_VALUES
)
),
};
},
@ -112,18 +129,26 @@ export default {
this.$set(this.value.spec, 'type', serviceType);
}
}
},
},
showAffinityTimeout() {
return this.value.spec.sessionAffinity === 'ClientIP' && !isEmpty(this.value.spec.sessionAffinityConfig);
return (
this.value.spec.sessionAffinity === 'ClientIP' &&
!isEmpty(this.value.spec.sessionAffinityConfig)
);
},
hasClusterIp() {
return this.checkTypeIs('ClusterIP') || this.checkTypeIs('LoadBalancer') || this.checkTypeIs('NodePort');
}
return (
this.checkTypeIs('ClusterIP') ||
this.checkTypeIs('LoadBalancer') ||
this.checkTypeIs('NodePort')
);
},
},
watch: {
'value.spec.selector': 'updateMatchingPods',
'value.spec.sessionAffinity'(val) {
if (val === 'ClientIP') {
this.value.spec.sessionAffinityConfig = { clientIP: { timeoutSeconds: null } };
@ -139,7 +164,7 @@ export default {
) {
delete this.value.spec.sessionAffinityConfig.clientIP.timeoutSeconds;
}
}
},
},
created() {
@ -153,6 +178,37 @@ export default {
},
methods: {
updateMatchingPods: throttle(function() {
const { allPods, value: { spec: { selector = { } } } } = this;
if (isEmpty(selector)) {
this.matchingPods = {
matched: 0,
total: allPods.length,
none: true,
sample: null,
};
} else {
const match = matching(allPods, selector);
this.matchingPods = {
matched: match.length,
total: allPods.length,
none: match.length === 0,
sample: match[0] ? match[0].nameDisplay : null,
};
}
}, 250, { leading: true }),
async loadPods() {
try {
const inStore = this.$store.getters['currentProduct'].inStore;
this.allPods = await this.$store.dispatch(`${ inStore }/findAll`, { type: POD });
this.matchingPods.total = this.allPods.length;
} catch (e) { }
},
checkTypeIs(typeIn) {
const { serviceType } = this;
@ -187,8 +243,7 @@ export default {
this.value.spec.ports = this.targetPortsStrOrInt(this.value.spec.ports);
}
},
}
},
};
</script>
@ -201,10 +256,10 @@ export default {
:subtypes="defaultServiceTypes"
:validation-passed="true"
:errors="errors"
@error="e=>errors = e"
@error="(e) => (errors = e)"
@finish="save"
@cancel="done"
@select-type="(st) => serviceType = st"
@select-type="(st) => (serviceType = st)"
@apply-hooks="() => applyHooks('_beforeSaveHooks')"
>
<NameNsDescription v-if="!isView" :value="value" :mode="mode" />
@ -226,12 +281,18 @@ export default {
:mode="mode"
:label="t('servicesPage.externalName.input.label')"
:placeholder="t('servicesPage.externalName.placeholder')"
:required="true"
type="text"
/>
</div>
</div>
</Tab>
<Tab v-else name="define-service-ports" :label="t('servicesPage.ips.define')" :weight="10">
<Tab
v-else
name="define-service-ports"
:label="t('servicesPage.ips.define')"
:weight="10"
>
<ServicePorts
v-model="value.spec.ports"
class="col span-12"
@ -247,7 +308,9 @@ export default {
>
<div class="row">
<div class="col span-12">
<Banner v-if="showSelectorWarning" color="warning" :label="t('servicesPage.selectors.helpText')" />
<Banner :color="(matchingPods.none ? 'warning' : 'success')">
<span v-html="t('servicesPage.selectors.matchingPods.matchesSome', matchingPods)" />
</Banner>
</div>
</div>
<div class="row">
@ -258,24 +321,27 @@ export default {
:mode="mode"
:initial-empty-row="true"
:protip="false"
@input="e=>$set(value.spec, 'selector', e)"
@input="(e) => $set(value.spec, 'selector', e)"
/>
</div>
</div>
</Tab>
<Tab name="ips" :label="t('servicesPage.ips.label')" :tooltip="t('servicesPage.ips.external.protip')">
<div
v-if="hasClusterIp"
class="row mb-20"
>
<Tab
name="ips"
:label="t('servicesPage.ips.label')"
:tooltip="t('servicesPage.ips.external.protip')"
>
<div v-if="hasClusterIp" class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model="value.spec.clusterIP"
:mode="mode"
:label="t('servicesPage.ips.input.label')"
:placeholder="t('servicesPage.ips.input.placeholder')"
:tooltip-key="hasClusterIp ? 'servicesPage.ips.clusterIpHelpText' : null"
@input="e=>$set(value.spec, 'clusterIP', e)"
:tooltip-key="
hasClusterIp ? 'servicesPage.ips.clusterIpHelpText' : null
"
@input="(e) => $set(value.spec, 'clusterIP', e)"
/>
</div>
</div>
@ -288,7 +354,7 @@ export default {
:value-placeholder="t('servicesPage.ips.external.placeholder')"
:mode="mode"
:protip="false"
@input="e=>$set(value.spec, 'externalIPs', e)"
@input="(e) => $set(value.spec, 'externalIPs', e)"
/>
</div>
</div>
@ -313,10 +379,22 @@ export default {
<div v-if="showAffinityTimeout" class="col span-6">
<UnitInput
v-model="value.spec.sessionAffinityConfig.clientIP.timeoutSeconds"
:suffix="t('suffix.seconds', {count: value.spec.sessionAffinityConfig.clientIP.timeoutSeconds})"
:suffix="
t('suffix.seconds', {
count:
value.spec.sessionAffinityConfig.clientIP.timeoutSeconds,
})
"
:label="t('servicesPage.affinity.timeout.label')"
:placeholder="t('servicesPage.affinity.timeout.placeholder')"
@input="e=>$set(value.spec.sessionAffinityConfig.clientIP, 'timeoutSeconds', e)"
@input="
(e) =>
$set(
value.spec.sessionAffinityConfig.clientIP,
'timeoutSeconds',
e
)
"
/>
</div>
</div>

165
mixins/auth.js Normal file
View File

@ -0,0 +1,165 @@
import { NORMAN, MANAGEMENT } from '@/config/types';
import { addObject, findBy } from '@/utils/array';
export default {
async fetch() {
const NAME = this.$route.params.id;
const originalModel = await this.$store.dispatch('rancher/find', {
type: NORMAN.AUTH_CONFIG,
id: NAME,
opt: { url: `/v3/${ NORMAN.AUTH_CONFIG }/${ NAME }`, force: true }
});
const serverUrl = await this.$store.dispatch('management/find', {
type: MANAGEMENT.SETTING,
id: 'server-url',
opt: { url: `/v1/{ MANAGEMENT.SETTING }/server-url` }
});
if ( serverUrl ) {
this.serverSetting = serverUrl.value;
}
this.model = await this.$store.dispatch(`rancher/clone`, { resource: originalModel });
if (NAME === 'shibboleth' && !this.model.openLdapConfig) {
this.model.openLdapConfig = {};
this.showLdap = false;
}
},
data() {
return { isSaving: false };
},
computed: {
me() {
const out = findBy(this.principals, 'me', true);
return out;
},
serverUrl() {
if ( this.serverSetting ) {
return this.serverSetting;
} else if ( process.client ) {
return window.location.origin;
}
return '';
},
principal() {
return this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, this.$store.getters['auth/principalId']) || {};
},
displayName() {
return this.t(`model.authConfig.provider.${ this.NAME }`);
},
NAME() {
return this.$route.params.id;
},
AUTH_CONFIG() {
return MANAGEMENT.AUTH_CONFIG;
}
},
methods: {
async save(btnCb) {
const configType = this.value.configType;
this.isSaving = true;
this.errors = [];
const wasEnabled = this.model.enabled;
let obj = this.toSave;
if (!obj) {
obj = this.model;
}
try {
if ( !wasEnabled ) {
if (configType === 'oauth') {
const code = await this.$store.dispatch('auth/test', { provider: this.model.id, body: this.model });
this.model.enabled = true;
obj.code = code;
}
if (configType === 'saml') {
this.model.enabled = true;
if (!this.model.accessMode) {
this.model.accessMode = 'unrestricted';
}
await this.model.save();
await this.model.doAction('testAndEnable', obj);
} else {
this.model.enabled = true;
if (!this.model.accessMode) {
this.model.accessMode = 'unrestricted';
}
await this.model.doAction('testAndApply', obj);
}
// Reload principals to get the new ones from the provider
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 (configType === 'oauth') {
await this.model.save();
await this.reloadModel();
}
this.isSaving = false;
btnCb(true);
if ( wasEnabled ) {
this.done();
}
// this.$router.applyQuery( { mode: 'view' } );
} catch (err) {
this.errors = [err];
btnCb(false);
this.model.enabled = wasEnabled;
this.isSaving = false;
}
},
async disable(btnCb) {
try {
if (this.model.hasAction('disable')) {
await this.model.doAction('disable');
} else {
const clone = await this.$store.dispatch(`rancher/clone`, { resource: this.model });
clone.enabled = false;
await clone.save();
}
await this.reloadModel();
btnCb(true);
} catch (err) {
this.errors = [err];
btnCb(false);
}
},
async reloadModel() {
this.originalModel = await this.$store.dispatch('rancher/find', {
type: NORMAN.AUTH_CONFIG,
id: this.NAME,
opt: { url: `/v3/${ NORMAN.AUTH_CONFIG }/${ this.NAME }`, force: true }
});
this.model = await this.$store.dispatch(`rancher/clone`, { resource: this.originalModel });
return this.model;
},
},
};

View File

@ -1,4 +1,5 @@
import { insertAt } from '@/utils/array';
import { set } from '@/utils/object';
const configType = {
activedirectory: 'ldap',
@ -61,5 +62,27 @@ export default {
await this.save();
this.currentRouter().push({ name: 'c-cluster-auth-config' });
};
},
applyDefaults() {
return () => {
switch (this.configType) {
case 'saml':
set(this, 'accessMode', 'unrestricted');
if (this.id === 'shibboleth' && !this.openLdapConfig) {
set(this, 'openLdapConfig', {});
}
break;
case 'ldap':
set(this, 'servers', []);
set(this, 'accessMode', 'unrestricted');
set(this, 'starttls', false);
break;
default:
break;
}
};
}
};

View File

@ -1,9 +1,41 @@
import SYSTEM_NAMESPACES from '@/config/system-namespaces';
import { PROJECT, SYSTEM_NAMESPACE } from '@/config/labels-annotations';
import { MANAGEMENT } from '@/config/types';
import { PROJECT, SYSTEM_NAMESPACE, ISTIO as ISTIO_LABELS } from '@/config/labels-annotations';
import { ISTIO, MANAGEMENT } from '@/config/types';
import { escapeHtml } from '@/utils/string';
import { insertAt, isArray } from '@/utils/array';
export default {
_availableActions() {
const out = this._standardActions;
insertAt(out, 0, { divider: true });
if (this.istioInstalled) {
insertAt(out, 0, {
action: 'enableAutoInjection',
label: this.t('namespace.enableAutoInjection'),
bulkable: true,
bulkAction: 'enableAutoInjection',
enabled: !this.injectionEnabled,
icon: 'icon icon-plus',
weight: 2
});
insertAt(out, 0, {
action: 'disableAutoInjection',
label: this.t('namespace.disableAutoInjection'),
bulkable: true,
bulkAction: 'disableAutoInjection',
enabled: this.injectionEnabled,
icon: 'icon icon-minus',
weight: 1,
});
}
return out;
},
isSystem() {
if ( this.metadata?.annotations?.[SYSTEM_NAMESPACE] === 'true' ) {
return true;
@ -51,5 +83,40 @@ export default {
projectNameSort() {
return this.project?.nameSort || '';
}
},
istioInstalled() {
const schema = this.$rootGetters['cluster/schemaFor'](ISTIO.GATEWAY);
return !!schema;
},
injectionEnabled() {
return this.labels[ISTIO_LABELS.AUTO_INJECTION] === 'enabled';
},
enableAutoInjection() {
return (namespaces = this, enable = true) => {
if (!isArray(namespaces)) {
namespaces = [namespaces];
}
namespaces.forEach((ns) => {
if (!enable && ns?.metadata?.labels) {
delete ns.metadata.labels[ISTIO_LABELS.AUTO_INJECTION];
} else {
if (!ns.metadata.labels) {
ns.metadata.labels = {};
}
ns.metadata.labels[ISTIO_LABELS.AUTO_INJECTION] = 'enabled';
}
ns.save();
});
};
},
disableAutoInjection() {
return (namespaces = this) => {
this.enableAutoInjection(namespaces, false);
};
},
};

View File

@ -0,0 +1,28 @@
import { colorForState, stateDisplay } from '@/plugins/steve/resource-instance';
import { findBy } from '@/utils/array';
import { get } from '@/utils/object';
export default {
readyMessage() {
const conditions = get(this, 'status.conditions');
const readyMessage = (findBy(conditions, 'type', 'Ready') || {}).message ;
return readyMessage;
},
colorForState() {
if (this.readyMessage) {
return colorForState(this.readyMessage);
}
return colorForState();
},
stateDisplay() {
if (this.readyMessage) {
return stateDisplay(this.readyMessage);
}
return stateDisplay();
}
};

View File

@ -1,5 +1,30 @@
import { colorForState, stateDisplay } from '@/plugins/steve/resource-instance';
import { findBy } from '@/utils/array';
import { get } from '@/utils/object';
export default {
canUpdate() {
return this?.metadata?.state?.error;
},
readyMessage() {
const conditions = get(this, 'status.conditions');
const readyMessage = (findBy(conditions, 'type', 'Ready') || {}).message ;
return readyMessage;
},
colorForState() {
if (this.readyMessage) {
return colorForState(this.readyMessage);
}
return colorForState();
},
stateDisplay() {
if (this.readyMessage) {
return stateDisplay(this.readyMessage);
}
return stateDisplay();
}
};

View File

@ -1,38 +1,67 @@
<script>
import { mapGetters } from 'vuex';
import { MANAGEMENT } from '@/config/types';
import PromptChangePassword from '@/components/PromptChangePassword';
import { NORMAN, MANAGEMENT } from '@/config/types';
import Loading from '@/components/Loading';
import { mapGetters } from 'vuex';
import ResourceTable from '@/components/ResourceTable';
export default {
components: {
Loading,
ResourceTable,
PromptChangePassword, Loading, ResourceTable
},
async fetch() {
this.rows = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.TOKEN, opt: { force: true } });
this.canChangePassword = await this.calcCanChangePassword();
this.apiKeys = await this.fetchApiKeys();
},
data() {
return { rows: null };
return {
apiKeys: null,
canChangePassword: false
};
},
computed: {
computed: {
...mapGetters({ t: 'i18n/t' }),
headers() {
return this.$store.getters['type-map/headersFor'](this.schema);
apiKeyheaders() {
return this.$store.getters['type-map/headersFor'](this.apiKeySchema);
},
schema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/schemaFor`](MANAGEMENT.TOKEN);
apiKeySchema() {
return this.$store.getters[`management/schemaFor`](MANAGEMENT.TOKEN);
},
apiKeys() {
principal() {
return this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, this.$store.getters['auth/principalId']) || {};
},
},
methods: {
addKey() {
this.$router.push({ path: 'account/create-key' });
},
async calcCanChangePassword() {
if (!this.$store.getters['auth/enabled']) {
return false;
}
if (this.principal.provider === 'local') {
return !!this.principal.loginName;
}
const users = await this.$store.dispatch('rancher/findAll', {
type: NORMAN.USER,
opt: { url: '/v3/users', filter: { me: true } }
});
if (users && users.length === 1) {
return !!users[0].username;
}
return false;
},
async fetchApiKeys() {
const rows = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.TOKEN, opt: { force: true } });
// Filter out tokens that are not API Keys
const isApiKey = (key) => {
const labels = key.metadata?.labels;
@ -48,48 +77,65 @@ export default {
return kind !== 'session' && !key.current;
};
return !this.rows ? [] : this.rows.filter(isApiKey);
},
},
methods: {
addKey() {
this.$router.push({ path: 'account/create-key' });
},
},
return !rows ? [] : rows.filter(isApiKey);
}
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<header>
<div class="title">
<h1 v-t="'account.apiKeys.title'" class="m-0"></h1>
</div>
<div class="actions-container">
<div class="actions">
<button class="btn role-primary" @click="addKey">
{{ t('account.apiKeys.add.label') }}
</button>
<h1 v-t="'accountAndKeys.title'" />
<section class="account">
<h4 v-t="'accountAndKeys.account.title'" />
<div class="content">
<div class="col mt-10">
<div><t k="accountAndKeys.account.name" />: {{ principal.name }}</div>
<div><t k="accountAndKeys.account.username" />: {{ principal.loginName }}</div>
</div>
<button
v-if="canChangePassword"
type="button"
class="btn role-secondary"
@click="$refs.promptChangePassword.show(true)"
>
{{ t("accountAndKeys.account.change") }}
</button>
</div>
</header>
<ResourceTable
:schema="schema"
:rows="apiKeys"
:headers="headers"
key-field="id"
:search="false"
:row-actions="true"
:table-actions="true"
/>
<PromptChangePassword ref="promptChangePassword" />
</section>
<section>
<h4 v-t="'accountAndKeys.keys.title'" />
<!-- account.apiKeys.title -->
<!-- account.apiKeys.add.label -->
<!-- -->
<button class="btn role-primary" @click="addKey">
{{ t('account.apiKeys.add.label') }}
</button>
<ResourceTable
:schema="apiKeySchema"
:rows="apiKeys"
:headers="apiKeyheaders"
key-field="id"
:search="false"
:row-actions="true"
:table-actions="true"
/>
</section>
</div>
</template>
<style lang="scss" scoped>
hr {
margin: 20px 0;
<style lang='scss' scoped>
section {
margin-bottom: 10px;
}
.account {
.content {
display: flex;
}
}
</style>

View File

@ -100,6 +100,9 @@ export default {
</div>
</div>
</span>
<div v-if="!kialiUrl" class="disabled-msg">
<span v-html="t('istio.links.disabled', {app: 'Kiali'})" />
</div>
</div>
<div :class="{'disabled':!jaegerUrl}" class="box link-container">
<span
@ -127,6 +130,9 @@ export default {
</div>
</div>
</span>
<div v-if="!jaegerUrl" class="disabled-msg">
<span v-html="t('istio.links.disabled', {app: 'Jaeger'})" />
</div>
</div>
</div>
</div>

View File

@ -166,7 +166,6 @@ export default {
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>

View File

@ -21,6 +21,7 @@ function SteveFactory(namespace, baseUrl) {
},
types: {},
socket: null,
queue: [],
wantSocket: false,
pendingSends: [],
started: [],

View File

@ -777,7 +777,6 @@ export default {
if ( !opt.url ) {
opt.url = this.actionLinkFor(actionName);
}
opt.method = 'post';
opt.data = body;

View File

@ -5,7 +5,8 @@ import { open, popupWindowOptions } from '@/utils/window';
import {
BACK_TO, SPA, AUTH_TEST, _FLAGGED, GITHUB_SCOPE, GITHUB_NONCE, GITHUB_REDIRECT
} from '@/config/query-params';
import { BASE_SCOPES } from '@/store/github';
export const BASE_SCOPES = { github: ['read:org'], googleoauth: ['email'] };
const KEY = 'rc_nonce';
@ -143,7 +144,7 @@ export const actions = {
const fromQuery = unescape(parseUrl(redirectUrl).query?.[GITHUB_SCOPE] || '');
const scopes = fromQuery.split(/[, ]+/).filter(x => !!x);
addObjects(scopes, BASE_SCOPES);
addObjects(scopes, BASE_SCOPES[provider]);
if ( opt.scopes ) {
addObjects(scopes, opt.scopes);

View File

@ -5,7 +5,6 @@ import { GITHUB_REPOS, GITHUB_SCOPES, _DATE } from '@/config/local-storage';
const API_BASE = 'https://api.github.com/';
export const BASE_SCOPES = ['read:org'];
export const EXTENDED_SCOPES = ['repo'];
export const DOCKERFILE = /^Dockerfile(\..*)?$/i;