From a57ebcaf01e561964710007b95c0f1a27dda24bb Mon Sep 17 00:00:00 2001 From: Mason Date: Wed, 3 Apr 2019 09:04:48 -0700 Subject: [PATCH 01/12] Cloud auth key UI elements --- .../galaxy/scripts/components/MaskedInput.js | 29 +++ .../components/User/CloudAuth/CloudAuth.scss | 61 ++++++ .../User/CloudAuth/CloudAuth.test.js | 53 +++++ .../components/User/CloudAuth/CloudAuth.vue | 188 ++++++++++++++++++ .../User/CloudAuth/CloudAuthItem.scss | 70 +++++++ .../User/CloudAuth/CloudAuthItem.vue | 88 ++++++++ .../User/CloudAuth/CredentialConfig.vue | 45 +++++ .../User/CloudAuth/CredentialForm.vue | 103 ++++++++++ .../components/User/CloudAuth/index.js | 1 + .../User/CloudAuth/model/AwsConfig.js | 27 +++ .../User/CloudAuth/model/AzureConfig.js | 60 ++++++ .../User/CloudAuth/model/AzureConfig.test.js | 21 ++ .../User/CloudAuth/model/BaseModel.js | 109 ++++++++++ .../User/CloudAuth/model/Credential.js | 111 +++++++++++ .../User/CloudAuth/model/Credential.test.js | 114 +++++++++++ .../User/CloudAuth/model/IdentityProvider.js | 30 +++ .../User/CloudAuth/model/ResourceProviders.js | 14 ++ .../components/User/CloudAuth/model/index.js | 3 + .../User/CloudAuth/model/service.js | 78 ++++++++ .../components/User/CloudAuth/operations.scss | 30 +++ .../CloudAuth/testdata/listCredentials.json | 17 ++ .../User/CloudAuth/testdata/mount.html | 39 ++++ .../User/CloudAuth/testdata/notes.txt | 31 +++ .../User/CloudAuth/testdata/sample.json | 38 ++++ .../User/CloudAuth/testdata/shallowMount.html | 41 ++++ .../scripts/entry/analysis/AnalysisRouter.js | 17 +- .../scripts/mvc/user/user-preferences.js | 8 + client/galaxy/scripts/utils/safeAssign.js | 11 + client/galaxy/style/scss/mixins.scss | 55 +++++ client/package.json | 1 + client/webpack.config.js | 5 +- client/yarn.lock | 5 + 32 files changed, 1497 insertions(+), 6 deletions(-) create mode 100644 client/galaxy/scripts/components/MaskedInput.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/CloudAuth.scss create mode 100644 client/galaxy/scripts/components/User/CloudAuth/CloudAuth.test.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/CloudAuth.vue create mode 100644 client/galaxy/scripts/components/User/CloudAuth/CloudAuthItem.scss create mode 100644 client/galaxy/scripts/components/User/CloudAuth/CloudAuthItem.vue create mode 100644 client/galaxy/scripts/components/User/CloudAuth/CredentialConfig.vue create mode 100644 client/galaxy/scripts/components/User/CloudAuth/CredentialForm.vue create mode 100644 client/galaxy/scripts/components/User/CloudAuth/index.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/model/AwsConfig.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.test.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/model/BaseModel.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/model/Credential.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/model/Credential.test.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/model/IdentityProvider.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/model/ResourceProviders.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/model/index.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/model/service.js create mode 100644 client/galaxy/scripts/components/User/CloudAuth/operations.scss create mode 100644 client/galaxy/scripts/components/User/CloudAuth/testdata/listCredentials.json create mode 100644 client/galaxy/scripts/components/User/CloudAuth/testdata/mount.html create mode 100644 client/galaxy/scripts/components/User/CloudAuth/testdata/notes.txt create mode 100644 client/galaxy/scripts/components/User/CloudAuth/testdata/sample.json create mode 100644 client/galaxy/scripts/components/User/CloudAuth/testdata/shallowMount.html create mode 100644 client/galaxy/scripts/utils/safeAssign.js diff --git a/client/galaxy/scripts/components/MaskedInput.js b/client/galaxy/scripts/components/MaskedInput.js new file mode 100644 index 00000000000..67f652e18a7 --- /dev/null +++ b/client/galaxy/scripts/components/MaskedInput.js @@ -0,0 +1,29 @@ +import BInput from "bootstrap-vue/es/components/form-input/form-input"; +import { createMask } from "imask"; + +export default { + extends: BInput, + props: { + mask: { + type: String, + required: false, + default: "" + } + }, + computed: { + masker() { + return createMask({ + mask: this.mask + }); + } + }, + methods: { + getFormatted(value) { + let result = this.stringifyValue(value); + if (this.mask.length) { + return this.masker.resolve(result); + } + return result; + } + } +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.scss b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.scss new file mode 100644 index 00000000000..9a79efd2e03 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.scss @@ -0,0 +1,61 @@ +@import "scss/mixins"; +@import "./operations"; + + +// General Layout + +.cloud-auth { + + // header sticks, bottom part scrolls + @include scrollingListLayout("header", ".scroll-container"); + + // title left, icons right + .cloud-auth-title { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + } + + // menu + .operations { + .help a { + @include fontawesome($fa-var-info-circle); + } + .filter a { + @include fontawesome($fa-var-filter); + } + .create a { + @include fontawesome($fa-var-plus); + } + } +} + + +// Transitions + +.fade-enter-active, +.fade-leave-active { + transition: opacity .5s; +} + +.fade-enter, +.fade-leave-to { + opacity: 0; +} + + +// Unhose base.scss hacks + +.cloud-auth div.form-row label { + font-weight: 400; +} + +#deleteCredentialModal { + .modal-body { + display: none; + } + .modal-dialog { + max-width: 300px; + } +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.test.js b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.test.js new file mode 100644 index 00000000000..d50979ced39 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.test.js @@ -0,0 +1,53 @@ +/* global expect */ +import sinon from "sinon"; + +import { default as CloudAuth, __RewireAPI__ as rewire } from "./CloudAuth"; +import CloudAuthItem from "./CloudAuthItem.vue"; + +import { mount, shallowMount, createLocalVue } from "@vue/test-utils"; +import _l from "utils/localization"; +import BootstrapVue from 'bootstrap-vue' + +// test data +import listCredentials from "./testdata/listCredentials.json"; + +const localVue = createLocalVue(); +localVue.use(BootstrapVue); +localVue.filter("localize", value => _l(value)); + +describe("CloudAuth component", () => { + + let wrapper, stub; + + let mockSvc = { + listCredentials: async () => null, + }; + + beforeEach(() => { + wrapper = shallowMount(CloudAuth, { localVue }); + rewire.__Rewire__("svc", mockSvc); + }); + + afterEach(() => { + if (stub) { + stub.restore(); + } + }); + + describe("initialization", () => { + + // fake list load + stub = sinon.stub(mockSvc, "listCredentials").resolves(listCredentials); + + it("should mount", () => { + assert(wrapper); + }) + + it("should show one item", () => { + let keyItems = wrapper.findAll(".cloud-auth-key"); + let firstitem = keyItems.at(0); + assert(keyItems); + }) + }) + +}) \ No newline at end of file diff --git a/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.vue b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.vue new file mode 100644 index 00000000000..5e270dffeaf --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.vue @@ -0,0 +1,188 @@ + + + + + \ No newline at end of file diff --git a/client/galaxy/scripts/components/User/CloudAuth/CloudAuthItem.scss b/client/galaxy/scripts/components/User/CloudAuth/CloudAuthItem.scss new file mode 100644 index 00000000000..72c06eb49af --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CloudAuthItem.scss @@ -0,0 +1,70 @@ +@import "scss/theme/blue.scss"; +@import "scss/mixins"; +@import "./operations"; + + +.cloud-auth-key { + + header hgroup { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + h4 { + cursor: pointer; + user-select: none; + margin-bottom: 0; + } + } + + form { + .custom-select { + background: none; // removes wierd double arrows on select + } + footer { + display: flex; + flex-direction: row; + justify-content: flex-end; + } + } + + + // icon menu + + .operations { + .save a { + @include fontawesome($fa-var-save) + } + .delete a { + @include fontawesome($fa-var-times); + } + .details a { + @include fontawesome($fa-var-window-minimize); + &:hover { + @include fontawesome($fa-var-window-maximize); + } + } + } + + + // reverse icons when key is expanded + + &.expanded .details a { + @include fontawesome($fa-var-window-maximize); + &:hover { + @include fontawesome($fa-var-window-minimize); + } + } + + + // save disk changes color based on form valid/dirty + + &.valid .save a { + color: $state-success-text; + } + &:not(.valid) .save a { + color: $state-danger-text; + } + +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/CloudAuthItem.vue b/client/galaxy/scripts/components/User/CloudAuth/CloudAuthItem.vue new file mode 100644 index 00000000000..b9a9d9923da --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CloudAuthItem.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/client/galaxy/scripts/components/User/CloudAuth/CredentialConfig.vue b/client/galaxy/scripts/components/User/CloudAuth/CredentialConfig.vue new file mode 100644 index 00000000000..3abcc740ff7 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CredentialConfig.vue @@ -0,0 +1,45 @@ + + + + diff --git a/client/galaxy/scripts/components/User/CloudAuth/CredentialForm.vue b/client/galaxy/scripts/components/User/CloudAuth/CredentialForm.vue new file mode 100644 index 00000000000..e2d41821abf --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CredentialForm.vue @@ -0,0 +1,103 @@ + + + diff --git a/client/galaxy/scripts/components/User/CloudAuth/index.js b/client/galaxy/scripts/components/User/CloudAuth/index.js new file mode 100644 index 00000000000..3e5bd9184df --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/index.js @@ -0,0 +1 @@ +export { default as CloudAuth } from "./CloudAuth.vue"; diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/AwsConfig.js b/client/galaxy/scripts/components/User/CloudAuth/model/AwsConfig.js new file mode 100644 index 00000000000..11004e28e62 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/AwsConfig.js @@ -0,0 +1,27 @@ +import { safeAssign } from "utils/safeAssign"; +import { BaseModel } from "./BaseModel"; + +export class AwsConfig extends BaseModel { + constructor(props = {}) { + super(); + this.role_arn = ""; + safeAssign(this, props); + this.updateState(); + } +} + +AwsConfig.setValidator(function(model) { + let errors = {}; + if (!model.role_arn.length) { + errors.role_arn = "Missing role_arn"; + } + return errors; +}); + +AwsConfig.fields = { + "role_arn": { + label: "Role ARN", + description: "The Amazon resource name (ARN) of the role to be assumed by Galaxy.", + placeholder: "arn:aws:iam::XXXXXXXXXXXX:role/XXXXXXXXXXX" + } +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.js b/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.js new file mode 100644 index 00000000000..296a31a5a42 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.js @@ -0,0 +1,60 @@ +import { safeAssign } from "utils/safeAssign"; +import { BaseModel } from "./BaseModel"; + +export class AzureConfig extends BaseModel { + constructor(props = {}) { + super(); + this.tenant_id = ""; + this.client_id = ""; + this.client_secret = ""; + safeAssign(this, props); + this.updateState(); + } +} + +AzureConfig.setValidator(function(model) { + let errors = {}; + + if (model.tenant_id.length < 36) { + errors.tenant_id = "Tenant ID too short"; + } + if (!model.tenant_id) { + errors.tenant_id = "Missing Tenant ID"; + } + + if (model.client_id.length < 36) { + errors.client_id = "Client ID too short"; + } + if (!model.client_id) { + errors.client_id = "Missing client ID"; + } + + if (!model.client_secret) { + errors.client_secret = "Missing secret"; + } + + return errors; +}); + +AzureConfig.fields = { + "tenant_id": { + label: "Tenant ID", + mask: "********-****-****-****-************", + placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + maxlength: 36, + description: "Your Tenant ID (or Directory ID) on Azure." + }, + "client_id": { + label: "Client ID", + mask: "********-****-****-****-************", + placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + maxlength: 36, + description: "The Client ID (or Application ID) you defined for Galaxy on your Azure directory." + }, + "client_secret": { + label: "Client Secret", + mask: "", + placeholder: "Client Secret", + description: "A secret string you obtained from Azure portal that Galaxy can use to prove its identity when requesting tokens to access your resources." + } +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.test.js b/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.test.js new file mode 100644 index 00000000000..372382e406e --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.test.js @@ -0,0 +1,21 @@ +import { AzureConfig } from "./AzureConfig"; + +describe("AzureConfig", () => { + + it("should instantiate", () => { + let instance = new AzureConfig(); + assert(instance); + assert(!instance.dirty); + assert(!instance.valid); + }) + + it("should validate client_secret", () => { + let instance = new AzureConfig(); + instance.tenant_id = "abc"; + instance.client_secret = "abc"; + assert(instance.fieldValid("client_secret")); + assert(!instance.fieldValid("tenant_id")); + assert(!instance.fieldValid("client_id")); + }) + +}) \ No newline at end of file diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/BaseModel.js b/client/galaxy/scripts/components/User/CloudAuth/model/BaseModel.js new file mode 100644 index 00000000000..ed8d0cdd2f3 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/BaseModel.js @@ -0,0 +1,109 @@ +const lastState = new WeakMap(); +const validators = new WeakMap(); +const transients = new WeakMap(); +const counter = new WeakMap(); + +// Helps with Vue for-loop keys +let instanceCounter = 0; + +export class BaseModel { + + constructor() { + counter.set(this, instanceCounter++); + } + + + /* object ID */ + + get counter() { + return counter.get(this); + } + + + /* Dirty state tracking */ + + get dirty() { + return lastState.get(this) != this.state; + } + + get clean() { + return !this.dirty; + } + + get state() { + // build state JSON ignoring transient fields + let tFields = this.transientFields; + return JSON.stringify(this, function(key) { + if (!tFields.has(key)) { + return this[key]; + } + return undefined; + }); + } + + get lastState() { + return lastState.get(this); + } + + get transientFields() { + let key = this.constructor; + if (!transients.has(key)) { + transients.set(key, new Set()); + } + return transients.get(key); + } + + updateState() { + lastState.set(this, this.state); + } + + // Setting a property name as transient for a class means its + // state will be caclulated without regard to that propety + static setTransient(...fieldNames) { + + let klass = this; // this will be a class + if (!transients.has(klass)) { + transients.set(klass, new Set()); + } + + let fields = transients.get(klass); + fieldNames.forEach(fieldName => fields.add(fieldName)) + transients.set(klass, fields); + } + + + + /* Validation */ + + get valid() { + return Object.keys(this.validationErrors).length == 0; + } + + get validationErrors() { + return this.constructor.validate(this); + } + + errorMessage(field) { + if (field in this.validationErrors) { + return this.validationErrors[field]; + } + return "errmessage"; + } + + fieldValid(fieldName) { + return !(fieldName in this.validationErrors); + } + + static validate(model) { + if (validators.has(this)) { + let validator = validators.get(this); + return validator(model); + } + throw new Error("Missing validator"); + } + + static setValidator(validationFunction) { + validators.set(this, validationFunction); + } + +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/Credential.js b/client/galaxy/scripts/components/User/CloudAuth/model/Credential.js new file mode 100644 index 00000000000..1ea556aee4b --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/Credential.js @@ -0,0 +1,111 @@ +import { safeAssign } from "utils/safeAssign"; +import { ResourceProviders } from "./ResourceProviders"; +import { BaseModel } from "./BaseModel"; + +export class Credential extends BaseModel { + + constructor(props = {}) { + super(); + + this.id = null; + this.description = ""; + this.authn_id = null; // identity provider + this.provider = null; // resource provider + + // transient props, exclude from state + this.expanded = false; + this.loading = false; + + // populate props + let options = Object.assign({}, Credential.defaults, props); + safeAssign(this, options); + + // init nested config + this.config = new this.configClass(options.config); + + // initialize state + this.updateState(); + } + + get title() { + return this.description.length ? this.description : this.provider; + } + + get valid() { + return super.valid && this.config.valid; + } + + + // Alias for provider also changes config object when updated + // Set this when updating in the UI + + get resourceProvider() { + return this.provider; + } + + set resourceProvider(newProvider) { + this.provider = newProvider; + this.config = new this.configClass({}); + } + + + // Polymorphic config class + + get configClass() { + return ResourceProviders.get(this.provider).klass; + } + + + // Methods + + filter(searchText = "") { + // TODO: better text filtering + return searchText.length + ? this.title.includes(searchText) : true; + } + + + // Statics + + static get defaults() { + return { + authn_id: null, + provider: 'aws', + expanded: false + }; + } + + static create(props = {}) { + return new Credential(props); + } +} + +/** + * Transient fields do not factor into the state + * for purposes of dirty tracking + */ +Credential.setTransient("expanded", "loading"); + +/** + * A validator function's job is to return an object + * where the keys are field names and the values are + * error messages. + */ +Credential.setValidator(function(model) { + + let errors = {}; + + if (!model.provider) { + errors.provider = "Provider must be set"; + } + + if (!model.authn_id) { + errors.authn_id = "Please pick an identity provider"; + } + + if (!model.config.valid) { + errors.config = "Invalid config object"; + } + + return errors; +}) \ No newline at end of file diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/Credential.test.js b/client/galaxy/scripts/components/User/CloudAuth/model/Credential.test.js new file mode 100644 index 00000000000..18789cb7372 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/Credential.test.js @@ -0,0 +1,114 @@ +import { Credential } from "./Credential"; +import { ResourceProviders } from "./ResourceProviders"; + +describe("Credential model", () => { + + describe("basic model props", () => { + + it("should exist", () => { + assert(Credential); + }) + + it("should build with defaults", () => { + let instance = new Credential(); + assert(instance); + }) + + it("should build with props", () => { + let description = "i am the test description"; + let props = { description }; + let instance = new Credential(props); + assert(instance); + assert(instance.description == description); + }) + + it("default config object should be AWS", () => { + let instance = new Credential(); + assert('role_arn' in instance.config, "Missing role_arn prop"); + assert(instance.config.constructor.name == "AwsConfig"); + }) + + it("should load a different config object given the right provider", () => { + let provider = 'azure'; + let props = { provider }; + let instance = new Credential(props); + assert(instance.provider == provider); + assert(instance.config.constructor.name == "AzureConfig"); + }) + + it("should dynamically switch config objects as resourceProvider is changed", () => { + let instance = new Credential(); + assert(instance.provider == 'aws', "Should default to AWS config' "); + instance.resourceProvider = 'azure'; + assert(instance.config.constructor.name == "AzureConfig", "Should have been AzureConfig"); + instance.resourceProvider = 'aws'; + assert(instance.config.constructor.name == "AwsConfig", "Should be AWS Config again"); + }) + }) + + describe("dirty state", () => { + + it("should flag as dirty when a prop is changed", () => { + let instance = new Credential(); + instance.description = "foo"; + assert(instance.dirty); + }) + + it("should flag as clean when a prop is restored", () => { + let instance = new Credential(); + instance.description = 'foo'; + assert(instance.dirty); + instance.description = ""; + assert(!instance.dirty); + }) + + it("should not flag as dirty when a transient prop is changed", () => { + let instance = new Credential(); + instance.loading = true; + assert(!instance.dirty); + }) + + }) + + describe("validation", () => { + + it("a new object should be invalid", () => { + let instance = new Credential(); + assert(!instance.valid); + }) + + it("it should become valid when props are assigned", () => { + let instance = new Credential(); + instance.authn_id = "floob"; + instance.resourceProvider = 'aws'; + instance.config.role_arn = "abc"; + assert(instance.valid); + }) + + it("it should be valid when initialized with correct props", () => { + let props = { + authn_id: "asdfasdf", + provider: 'aws', + config: { + role_arn: "floobar" + } + } + let instance = new Credential(props); + assert(instance.valid); + }) + + it("should be invalid when bad props assigned", () => { + let props = { + authn_id: "asdfasdf", + provider: 'aws', + config: { + role_arn: "" + } + } + let instance = new Credential(props); + assert(!instance.valid); + }) + + }) + +}) diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/IdentityProvider.js b/client/galaxy/scripts/components/User/CloudAuth/model/IdentityProvider.js new file mode 100644 index 00000000000..b78538bdb82 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/IdentityProvider.js @@ -0,0 +1,30 @@ +import { safeAssign } from "utils/safeAssign"; + +export class IdentityProvider { + + constructor(props = {}) { + this.id = ""; + this.provider = ""; + safeAssign(this, props); + } + + // Aliases + + get authn_id() { + return this.id; + } + + get text() { + return this.provider; + } + + get value() { + return this.id; + } + + // Statics + + static create(props = {}) { + return new IdentityProvider(props); + } +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/ResourceProviders.js b/client/galaxy/scripts/components/User/CloudAuth/model/ResourceProviders.js new file mode 100644 index 00000000000..965e254b2f1 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/ResourceProviders.js @@ -0,0 +1,14 @@ +import { AwsConfig } from "./AwsConfig"; +import { AzureConfig } from "./AzureConfig"; + +export const ResourceProviders = new Map(); + +ResourceProviders.set("aws", { + klass: AwsConfig, + label: "Amazon Web Services (AWS)" +}); + +ResourceProviders.set("azure", { + klass: AzureConfig, + label: "Microsoft Azure" +}); diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/index.js b/client/galaxy/scripts/components/User/CloudAuth/model/index.js new file mode 100644 index 00000000000..9f9f76db4a7 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/index.js @@ -0,0 +1,3 @@ +export { Credential } from "./Credential"; +export { IdentityProvider } from "./IdentityProvider"; +export { ResourceProviders } from "./ResourceProviders"; \ No newline at end of file diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/service.js b/client/galaxy/scripts/components/User/CloudAuth/model/service.js new file mode 100644 index 00000000000..f16a2ce21bb --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/service.js @@ -0,0 +1,78 @@ +/** + * Data retrieval/storage for the auth keys + */ + +import axios from "axios"; +import { Credential, IdentityProvider } from "./index"; +import { getRootFromIndexLink } from "onload"; + +const getUrl = path => getRootFromIndexLink() + path; + +export async function listCredentials() { + let url = getUrl("api/cloud/authz"); + let response = await axios.get(url); + if (response.status != 200) { + throw new Error("Bad mojo"); + } + return response.data.map(Credential.create); +} + +export async function getCredential(id) { + let url = getUrl("api/cloud/authz/${id}"); + let response = await axios.get(url); + if (response.status != 200) { + throw new Error("Bad load"); + } + return Credential.create(response.data); +} + +export async function saveCredential(newItem) { + let model = Credential.create(newItem); + let response = await saveOrUpdate(model); + if (response.status != 200) { + throw new Error("bad save"); + } + return Credential.create(response.data); +} + +async function saveOrUpdate(model) { + return model.id + ? axios.put(getUrl(`api/cloud/authz/${model.id}`), model) + : axios.post(getUrl("api/cloud/authz"), model); +} + +export async function deleteCredential(doomed) { + let model = Credential.create(doomed); + if (model.id) { + let url = getUrl(`api/cloud/authz/${doomed.id}`); + let response = await axios.delete(url); + if (response.status != 200) { + throw new Error("Bad delete"); + } + } + return model; +} + +// Memoize results (basically never changes) + +let identityProviders; + +export async function getIdentityProviders() { + if (!identityProviders) { + let url = getUrl("authnz"); + let response = await axios.get(url); + if (response.status != 200) { + throw new Error("Unable to load identity providers"); + } + identityProviders = response.data.map(IdentityProvider.create); + } + return identityProviders; +} + +export default { + listCredentials, + getCredential, + saveCredential, + deleteCredential, + getIdentityProviders +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/operations.scss b/client/galaxy/scripts/components/User/CloudAuth/operations.scss new file mode 100644 index 00000000000..305de4851ed --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/operations.scss @@ -0,0 +1,30 @@ +/** +I put the styles for this little icon menu in its own file because +I think this should probably become its own general component + +TODO: Build operations menu component for reuse wherever we do little +icon doodads +*/ + +// TODO: Show this to Sam, this is what I need to import +// to use ml-2 because our own base.scss is not importable +@import "~bootstrap/scss/functions"; +@import "~bootstrap/scss/variables"; +@import "~bootstrap/scss/mixins"; +@import "~bootstrap/scss/utilities/spacing"; + +@import "scss/theme/blue.scss"; +@import "scss/mixins"; + + +.operations { + margin-bottom: 0; + + ul { + @include list_reset(); + display: flex; + li:not(:first-child) { + @extend .ml-2; // terrible + } + } +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/testdata/listCredentials.json b/client/galaxy/scripts/components/User/CloudAuth/testdata/listCredentials.json new file mode 100644 index 00000000000..f4c6028668d --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/testdata/listCredentials.json @@ -0,0 +1,17 @@ +[ + { + "authn_id": "f2db41e1fa331b3e", + "user_id": "f2db41e1fa331b3e", + "deleted": "True", + "last_update": "2019-03-27 10:08:27.711092", + "last_activity": "2019-03-27 10:08:27.711104", + "create_time": "2019-03-27 17:08:27.712045", + "provider": "aws", + "model_class": "CloudAuthz", + "config": { + "role_arn": "arn:sws:idm::347162595" + }, + "description": "floob", + "id": "a799d38679e985db" + } +] \ No newline at end of file diff --git a/client/galaxy/scripts/components/User/CloudAuth/testdata/mount.html b/client/galaxy/scripts/components/User/CloudAuth/testdata/mount.html new file mode 100644 index 00000000000..4bfc953e16c --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/testdata/mount.html @@ -0,0 +1,39 @@ +

Cloud +Auth Keys

+

Lorem ipsum dolor sit amet, consectetur +adipiscing elit. +Fusce vehicula placerat ante et tincidunt. Donec congue +pellentesque tortor sagittis feugiat. Duis rhoncus arcu +id erat ultrices egestas. Suspendisse rutrum ipsum id

+
0 matches out +of 0 items
+
\ No newline at end of file diff --git a/client/galaxy/scripts/components/User/CloudAuth/testdata/notes.txt b/client/galaxy/scripts/components/User/CloudAuth/testdata/notes.txt new file mode 100644 index 00000000000..6ccb1a1af58 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/testdata/notes.txt @@ -0,0 +1,31 @@ +/authnz + +// options for authn dropdown + +[ + {"id": "f597429621d6eb2b", "provider": "google-openidconnect"}, + {"id": "f597429621d6eb2b", "provider": "google-openidconnect"}, + {"id": "f597429621d6eb2b", "provider": "google-openidconnect"}, + {"id": "f597429621d6eb2b", "provider": "google-openidconnect"}, + {"id": "f597429621d6eb2b", "provider": "google-openidconnect"} +] + +{ + "provider": "aws", + "authn_id":"f2db41e1fa331b3e", + "description": "asdfasdf", + "config": { + "role_arn": "arn:aws:iam::347162595075:role/CloudAuthzTests" + } +} + +{ + "provider": "azure", + "authn_id": "f2db41e1fa331b3e", + "description": "asdfasdf", + "config": { + 'tenant_id': TENANT_ID, + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET + } +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/testdata/sample.json b/client/galaxy/scripts/components/User/CloudAuth/testdata/sample.json new file mode 100644 index 00000000000..f5055687742 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/testdata/sample.json @@ -0,0 +1,38 @@ +[ + { + "authn_id": "f2db41e1fa331b3e", + "user_id": "f2db41e1fa331b3e", + "last_update": "2019-03-24 23:29:10.939661", + "last_activity": "2019-03-24 23:29:10.939670", + "provider": "aws", + "model_class": "CloudAuthz", + "config": { + "role_arn": "asdfadfasdf" + }, + "id": "ebfb8f50c6abde6d" + }, + { + "authn_id": "f2db41e1fa331b3e", + "user_id": "f2db41e1fa331b3e", + "last_update": "2019-03-24 23:25:51.678020", + "last_activity": "2019-03-24 23:25:51.678030", + "provider": "aws", + "model_class": "CloudAuthz", + "config": { + "role_arn": "asdfadfasdf" + }, + "id": "1cd8e2f6b131e891" + }, + { + "authn_id": "f2db41e1fa331b3e", + "user_id": "f2db41e1fa331b3e", + "last_update": "2019-03-23 15:43:14.511896", + "last_activity": "2019-03-23 15:43:14.511905", + "provider": "aws", + "model_class": "CloudAuthz", + "config": { + "role_arn": "asdfadfasdf" + }, + "id": "f597429621d6eb2b" + } +] \ No newline at end of file diff --git a/client/galaxy/scripts/components/User/CloudAuth/testdata/shallowMount.html b/client/galaxy/scripts/components/User/CloudAuth/testdata/shallowMount.html new file mode 100644 index 00000000000..dc226929e80 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/testdata/shallowMount.html @@ -0,0 +1,41 @@ +
+
+
+

Cloud Auth Keys

+ +
+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Fusce vehicula placerat ante et tincidunt. Donec congue + pellentesque tortor sagittis feugiat. Duis rhoncus arcu + id erat ultrices egestas. Suspendisse rutrum ipsum id

+
+ +
+ + + +
+
+ +
+ + + Create New Authorization + + +
+ + + + +
\ No newline at end of file diff --git a/client/galaxy/scripts/entry/analysis/AnalysisRouter.js b/client/galaxy/scripts/entry/analysis/AnalysisRouter.js index 3fc93a5cb39..5b0dea513c6 100644 --- a/client/galaxy/scripts/entry/analysis/AnalysisRouter.js +++ b/client/galaxy/scripts/entry/analysis/AnalysisRouter.js @@ -38,6 +38,7 @@ import DatasetEditAttributes from "mvc/dataset/dataset-edit-attributes"; import Citations from "components/Citations.vue"; import DisplayStructure from "components/DisplayStructured.vue"; import Vue from "vue"; +import { CloudAuth } from "components/User/CloudAuth"; /** Routes */ export const getAnalysisRouter = Galaxy => @@ -47,6 +48,7 @@ export const getAnalysisRouter = Galaxy => "(/)root*": "home", "(/)tours(/)(:tour_id)": "show_tours", "(/)user(/)": "show_user", + "(/)user(/)cloud_auth": "show_cloud_auth", "(/)user(/)(:form_id)": "show_user_form", "(/)pages(/)create(/)": "show_pages_create", "(/)pages(/)edit(/)": "show_pages_edit", @@ -75,18 +77,19 @@ export const getAnalysisRouter = Galaxy => "(/)datasets/error": "show_dataset_error" }, - require_login: ["show_user", "show_user_form", "show_workflows"], + require_login: ["show_user", "show_user_form", "show_workflows", "show_cloud_auth"], authenticate: function(args, name) { let Galaxy = getGalaxyInstance(); return (Galaxy.user && Galaxy.user.id) || this.require_login.indexOf(name) == -1; }, - _display_vue_helper: function(component, props) { + _display_vue_helper: function(component, props = {}) { let instance = Vue.extend(component); - let vm = document.createElement("div"); - this.page.display(vm); - new instance(props).$mount(vm); + let container = document.createElement("div"); + this.page.display(container); + let vm = new instance(props).$mount(container); + return vm; }, show_tours: function(tour_id) { @@ -109,6 +112,10 @@ export const getAnalysisRouter = Galaxy => this.page.display(new FormWrapper.View(_.extend(model.get(form_id), { active_tab: "user" }))); }, + show_cloud_auth: function() { + this._display_vue_helper(CloudAuth); + }, + show_visualizations: function(action_id) { var activeTab = action_id == "list_published" ? "shared" : "user"; this.page.display( diff --git a/client/galaxy/scripts/mvc/user/user-preferences.js b/client/galaxy/scripts/mvc/user/user-preferences.js index bdfe85d2a28..2ad23a268af 100644 --- a/client/galaxy/scripts/mvc/user/user-preferences.js +++ b/client/galaxy/scripts/mvc/user/user-preferences.js @@ -90,6 +90,13 @@ var Model = Backbone.Model.extend({ submit_title: "Create a new key", submit_icon: "fa-check" }, + cloud_auth: { + title: _l("Manage Cloud Authorization"), + description: _l("Add or modify the configuration that grants Galaxy to access your cloud-based resources."), + icon: "fa-cloud", + submit_title: "Create a new key", + submit_icon: "fa-check" + }, toolbox_filters: { title: _l("Manage Toolbox filters"), description: _l("Customize your Toolbox by displaying or omitting sets of Tools."), @@ -166,6 +173,7 @@ var View = Backbone.View.extend({ self._addLink("permissions"); self._addLink("make_data_private"); self._addLink("api_key"); + self._addLink("cloud_auth"); if (config.has_user_tool_filters) { self._addLink("toolbox_filters"); } diff --git a/client/galaxy/scripts/utils/safeAssign.js b/client/galaxy/scripts/utils/safeAssign.js new file mode 100644 index 00000000000..4f1db0bae4a --- /dev/null +++ b/client/galaxy/scripts/utils/safeAssign.js @@ -0,0 +1,11 @@ +// Object.assign, but props transferred must exist in target + +export function safeAssign(target, source = {}) { + if (!(source instanceof Object)) { + console.warn("safeAssign expected an object, instead got:", source); + source = {}; + } + Object.keys(source) + .filter(prop => target.hasOwnProperty(prop)) + .forEach(prop => target[prop] = source[prop]); +} diff --git a/client/galaxy/style/scss/mixins.scss b/client/galaxy/style/scss/mixins.scss index 6cdf41fd46f..81eb9c38141 100644 --- a/client/galaxy/style/scss/mixins.scss +++ b/client/galaxy/style/scss/mixins.scss @@ -1,3 +1,8 @@ +$fa-font-path: "~font-awesome/fonts/"; +@import "~font-awesome/scss/font-awesome.scss"; +@import "~font-awesome/scss/_mixins"; + + // Utility mixin expands to container edges @mixin fill() { position: relative; @@ -29,3 +34,53 @@ display: none; } } + +@mixin scrollMe() { + overflow-x: hidden; + overflow-y: scroll; + &::-webkit-scrollbar { + display: none; + } +} + +// top selector fixed, bottom list scrolls +@mixin scrollingListLayout($fixedTop, $listRegion) { + @include fill(); + + display: flex; + flex-direction: column; + + #{$fixedTop} { + flex-grow: 0; + } + + #{$listRegion} { + flex-grow: 1; + @include scrollMe(); + } +} + + +// Removes browser formatting of lists + +@mixin list_reset() { + list-style: none; + padding: 0 0 0 0; + margin: 0 0 0 0; +} + + +// font awesome as a mixin +// icon variables come from ~font-awesome/scss/_variables.scss + +@mixin fontawesome($icon) { + @include fa-icon(); + &:before { + content: $icon; + } + span { + // Span for text readers + // Label + @include sr-only(); + } +} diff --git a/client/package.json b/client/package.json index 18b026c6532..e9f93b7a57b 100644 --- a/client/package.json +++ b/client/package.json @@ -25,6 +25,7 @@ "decode-uri-component": "^0.2.0", "font-awesome": "^4.7.0", "handsontable": "^2.0.0", + "imask": "^4.1.5", "imports-loader": "^0.8.0", "jquery": "2", "jquery-migrate": "~1.4", diff --git a/client/webpack.config.js b/client/webpack.config.js index 18973b9d67b..20a17781fd9 100644 --- a/client/webpack.config.js +++ b/client/webpack.config.js @@ -162,7 +162,10 @@ let buildconfig = { loader: "sass-loader", options: { sourceMap: true, - includePaths: ["galaxy/style/scss"] + includePaths: [ + "galaxy/style/scss", + path.resolve(__dirname, './node_modules') + ] } } ] diff --git a/client/yarn.lock b/client/yarn.lock index baf011dc925..889f63f1d2a 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -6501,6 +6501,11 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +imask@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/imask/-/imask-4.1.5.tgz#9942993ece68b7b33d03e12781a7401b1207e52c" + integrity sha512-n9e3KkJxe6YZKueT02I8/tXpC0vzxmSzoBB5BPi6OPq0rp7qOZfXN5777Dkw6WEYRswlsxm+7pl7WocsG0LWyA== + immer@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" From f9225a6693142bd1b3e64e2f0d32003a537ec892 Mon Sep 17 00:00:00 2001 From: vahid Date: Tue, 26 Mar 2019 17:15:02 -0700 Subject: [PATCH 02/12] Add doc string for cloudauthz deserializer. --- lib/galaxy/managers/cloudauthzs.py | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/lib/galaxy/managers/cloudauthzs.py b/lib/galaxy/managers/cloudauthzs.py index 7c5ecc20144..aeac25b4e1c 100644 --- a/lib/galaxy/managers/cloudauthzs.py +++ b/lib/galaxy/managers/cloudauthzs.py @@ -61,3 +61,59 @@ class CloudAuthzsSerializer(base.ModelSerializer): 'last_activity': lambda i, k, **c: str(i.last_activity), 'create_time' : lambda i, k, **c: str(i.create_time) }) + + +class CloudAuthzsDeserializer(base.ModelDeserializer): + """ + Service object for validating and deserializing dictionaries that + update/alter cloudauthz configurations. + """ + model_manager_class = CloudAuthzManager + + def add_deserializers(self): + super(CloudAuthzsDeserializer, self).add_deserializers() + self.deserializers.update({ + 'authn_id': self.deserialize_and_validate_authn_id, + 'provider': self.default_deserializer, + 'config': self.default_deserializer, + 'deleted': self.default_deserializer + }) + + def deserialize_and_validate_authn_id(self, item, key, val, **context): + """ + Deserializes an authentication ID (authn_id), and asserts if the + current user can assume that authentication. + + :type item: galaxy.model.CloudAuthz + :param item: an instance of cloudauthz + + :type key: string + :param key: `authn_id` attribute of the cloudauthz object (i.e., the `item` param). + + :type val: string + :param val: the value of `authn_id` attribute of the cloudauthz object (i.e., the `item` param). + + :type context: dict + :param context: a dictionary object containing Galaxy `trans`. + + :rtype: string + :return: decoded authentication ID. + """ + + try: + decoded_authn_id = self.app.security.decode_id(val) + except Exception: + log.debug("cannot decode authz_id `" + str(val) + "`") + raise MalformedId("Invalid `authz_id` {}!".format(val)) + + trans = context.get("trans") + if trans is None: + log.debug("Not found expected `trans` when deserializing CloudAuthz.") + raise InternalServerError + + try: + trans.app.authnz_manager.can_user_assume_authn(trans, decoded_authn_id) + except Exception as e: + raise e + + return decoded_authn_id From 49b58bb7abd5e0b9c3babc5b3dcf7863a790ef0a Mon Sep 17 00:00:00 2001 From: vahid Date: Tue, 26 Mar 2019 17:16:31 -0700 Subject: [PATCH 03/12] Add CloudAuthz PUT api and its router. --- lib/galaxy/webapps/galaxy/api/cloudauthz.py | 23 +++++++++++++++++++++ lib/galaxy/webapps/galaxy/buildapp.py | 6 ++++++ 2 files changed, 29 insertions(+) diff --git a/lib/galaxy/webapps/galaxy/api/cloudauthz.py b/lib/galaxy/webapps/galaxy/api/cloudauthz.py index c258846936b..19ffb12678f 100644 --- a/lib/galaxy/webapps/galaxy/api/cloudauthz.py +++ b/lib/galaxy/webapps/galaxy/api/cloudauthz.py @@ -35,6 +35,7 @@ class CloudAuthzController(BaseAPIController): super(CloudAuthzController, self).__init__(app) self.cloudauthz_manager = cloudauthzs.CloudAuthzManager(app) self.cloudauthz_serializer = cloudauthzs.CloudAuthzsSerializer(app) + self.cloudauthz_deserializer = cloudauthzs.CloudAuthzsDeserializer(app) @expose_api def index(self, trans, **kwargs): @@ -183,3 +184,25 @@ class CloudAuthzController(BaseAPIController): "ID: `{}`.".format(encoded_authz_id))) raise InternalServerError('An unexpected error has occurred while responding to the DELETE request of the ' 'cloudauthz API.' + str(e)) + + @expose_api + def update(self, trans, encoded_authz_id, payload, **kwargs): + msg_template = "Rejected user `" + str(trans.user.id) + "`'s request to delete cloudauthz config because of {}." + try: + authz_id = self.decode_id(encoded_authz_id) + except Exception: + log.debug(msg_template.format("cannot decode authz_id `" + str(encoded_authz_id) + "`")) + raise MalformedId('Invalid `authz_id`!') + + try: + cloudauthz_to_update = trans.app.authnz_manager.try_get_authz_config(trans.sa_session, trans.user.id, authz_id) + self.cloudauthz_deserializer.deserialize(cloudauthz_to_update, payload, trans=trans) + self.cloudauthz_serializer.serialize_to_view(cloudauthz_to_update, view='summary') + return self.cloudauthz_serializer.serialize_to_view(cloudauthz_to_update, view='summary') + except MalformedId as e: + raise e + except Exception as e: + log.exception(msg_template.format("exception while updating the cloudauthz record with " + "ID: `{}`.".format(encoded_authz_id))) + raise InternalServerError('An unexpected error has occurred while responding to the PUT request of the ' + 'cloudauthz API.' + str(e)) diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index ab118f2b82c..59d6ae65a69 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -315,6 +315,12 @@ def populate_api_routes(webapp, app): controller='cloudauthz', conditions=dict(method=["DELETE"])) + webapp.mapper.connect('upload_cloudauthz_item', + '/api/cloud/authz/{encoded_authz_id}', + action='update', + controller="cloudauthz", + conditions=dict(method=["PUT"])) + webapp.mapper.connect('get_custom_builds_metadata', '/api/histories/{id}/custom_builds_metadata', controller='histories', From 7f16994f96bb50d37657e0586272c32c40cb084d Mon Sep 17 00:00:00 2001 From: vahid Date: Tue, 26 Mar 2019 17:34:54 -0700 Subject: [PATCH 04/12] Add cloudauth PUT api documentation. --- lib/galaxy/webapps/galaxy/api/cloudauthz.py | 46 ++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/webapps/galaxy/api/cloudauthz.py b/lib/galaxy/webapps/galaxy/api/cloudauthz.py index 19ffb12678f..7467403dc53 100644 --- a/lib/galaxy/webapps/galaxy/api/cloudauthz.py +++ b/lib/galaxy/webapps/galaxy/api/cloudauthz.py @@ -69,13 +69,18 @@ class CloudAuthzController(BaseAPIController): :type payload: dict :param payload: A dictionary structure containing the following keys: * provider: the cloud-based resource provider to which this configuration belongs to. + * config: a dictionary containing all the configuration required to request temporary credentials - from the provider. + from the provider. See the following page for details: + https://galaxyproject.org/cloud/authnz/ + * authn_id: the (encoded) ID of a third-party authentication of a user. To have this ID, user must have logged-in to this Galaxy server using third-party identity (e.g., Google), or has associated his/her Galaxy account with a third-party OIDC-based identity. See this page: https://galaxyproject.org/admin/authentication/ + * description: [Optional] a brief description for this configuration. + :param kwargs: empty dict :rtype: dict @@ -187,6 +192,45 @@ class CloudAuthzController(BaseAPIController): @expose_api def update(self, trans, encoded_authz_id, payload, **kwargs): + """ + * PUT /api/cloud/authz/{encoded_authz_id} + Updates the values for the cloudauthz configuration with the given ``encoded_authz_id``. + + With this API only the following attributes of a cloudauthz configuration + can be updated: `authn_id`, `provider`, `config`, `deleted`. + + :type trans: galaxy.web.framework.webapp.GalaxyWebTransaction + :param trans: Galaxy web transaction + + :type encoded_authz_id: string + :param encoded_authz_id: The encoded ID of the CloudAuthz record to be updated. + + :type payload: dict + :param payload: A dictionary structure containing the attributes to modified with their new values. + It can contain any number of the following attributes: + * provider: the cloud-based resource provider + to which this configuration belongs to. + + * authn_id: the (encoded) ID of a third-party authentication of a user. + To have this ID, user must have logged-in to this Galaxy server + using third-party identity (e.g., Google), or has associated + their Galaxy account with a third-party OIDC-based identity. + See this page: https://galaxyproject.org/admin/authentication/ + + Note: A user can associate a cloudauthz record with their own + authentications only. If the given authentication with authn_id + belongs to a different user, Galaxy will throw the + ItemAccessibilityException exception. + + * config: a dictionary containing all the configuration required to + request temporary credentials from the provider. + See the following page for details: + https://galaxyproject.org/cloud/authnz/ + + * deleted: a boolean type marking the specified cloudauthz as (un)deleted. + + """ + msg_template = "Rejected user `" + str(trans.user.id) + "`'s request to delete cloudauthz config because of {}." try: authz_id = self.decode_id(encoded_authz_id) From e61b2e557e54554f5b5ead26a30b2d9be84a611f Mon Sep 17 00:00:00 2001 From: Mason Date: Wed, 3 Apr 2019 15:08:05 -0700 Subject: [PATCH 05/12] Added description field to cloudauth --- .../components/User/CloudAuth/CloudAuth.scss | 35 +++++++++++++------ .../components/User/CloudAuth/CloudAuth.vue | 35 ++++++++++++------- .../User/CloudAuth/CloudAuthItem.scss | 33 +++++++++++++---- .../User/CloudAuth/CloudAuthItem.vue | 22 ++++++++---- .../User/CloudAuth/CredentialConfig.vue | 2 +- .../User/CloudAuth/CredentialForm.vue | 30 +++++++++++++--- .../components/User/CloudAuth/operations.scss | 2 +- lib/galaxy/managers/cloudauthzs.py | 10 +++--- 8 files changed, 122 insertions(+), 47 deletions(-) diff --git a/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.scss b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.scss index 9a79efd2e03..ddafe5a8469 100644 --- a/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.scss +++ b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.scss @@ -15,18 +15,31 @@ flex-direction: row; justify-content: space-between; align-items: center; - } + + + // menu + .operations { - // menu - .operations { - .help a { - @include fontawesome($fa-var-info-circle); - } - .filter a { - @include fontawesome($fa-var-filter); - } - .create a { - @include fontawesome($fa-var-plus); + li { + a, a::before { + font-size: 1rem; + color: $gray-400; + } + a.active::before, + a:hover::before { + color: $brand-primary; + } + } + + .cloudKeyHelp a { + @include fontawesome($fa-var-info-circle); + } + .cloudKeyFilter a { + @include fontawesome($fa-var-filter); + } + .createCloudKey a { + @include fontawesome($fa-var-plus); + } } } } diff --git a/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.vue b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.vue index 5e270dffeaf..56b5372e111 100644 --- a/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.vue +++ b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.vue @@ -6,23 +6,25 @@

Manage Cloud Authorization