mirror of https://github.com/rancher/dashboard.git
Merge remote-tracking branch 'origin/update-user-password-2' into api-keys
This commit is contained in:
commit
2a117c8c10
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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…
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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--,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -80,6 +80,7 @@ export default {
|
|||
},
|
||||
|
||||
onBlur() {
|
||||
this.$emit('blur');
|
||||
this.onBlurLabeled();
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -170,7 +170,7 @@ export default {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.row {
|
||||
&.row {
|
||||
display: flex;
|
||||
.radio-container {
|
||||
margin-right: 10px;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default {
|
|||
|
||||
addPrefix: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: false
|
||||
},
|
||||
|
||||
suffix: {
|
||||
|
|
|
|||
|
|
@ -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 & 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;
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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) }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
|
|
|
|||
132
edit/service.vue
132
edit/service.vue
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -166,7 +166,6 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ function SteveFactory(namespace, baseUrl) {
|
|||
},
|
||||
types: {},
|
||||
socket: null,
|
||||
queue: [],
|
||||
wantSocket: false,
|
||||
pendingSends: [],
|
||||
started: [],
|
||||
|
|
|
|||
|
|
@ -777,7 +777,6 @@ export default {
|
|||
if ( !opt.url ) {
|
||||
opt.url = this.actionLinkFor(actionName);
|
||||
}
|
||||
|
||||
opt.method = 'post';
|
||||
opt.data = body;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue