diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index f11f1ad9751f..2584707d0d76 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -8,20 +8,6 @@ export interface paths { /** Returns returns an API key for authenticated user based on BaseAuth headers. */ get: operations["get_api_key_api_authenticate_baseauth_get"]; }; - "/api/cloud/storage/get": { - /** - * Gets given objects from a given cloud-based bucket to a Galaxy history. - * @deprecated - */ - post: operations["get_api_cloud_storage_get_post"]; - }; - "/api/cloud/storage/send": { - /** - * Sends given dataset(s) in a given history to a given cloud-based bucket. - * @deprecated - */ - post: operations["send_api_cloud_storage_send_post"]; - }; "/api/configuration": { /** * Return an object containing exposable configuration settings @@ -2797,85 +2783,6 @@ export interface components { /** Item Ids */ item_ids: string[]; }; - /** CloudDatasets */ - CloudDatasets: { - /** - * Authentication ID - * @description The ID of CloudAuthz to be used for authorizing access to the resource provider. You may get a list of the defined authorizations via `/api/cloud/authz`. Also, you can use `/api/cloud/authz/create` to define a new authorization. - * @example 0123456789ABCDEF - */ - authz_id: string; - /** - * Bucket - * @description The name of a bucket to which data should be sent (e.g., a bucket name on AWS S3). - */ - bucket: string; - /** - * Objects - * @description A list of dataset IDs belonging to the specified history that should be sent to the given bucket. If not provided, Galaxy sends all the datasets belonging the specified history. - */ - dataset_ids?: string[] | null; - /** - * History ID - * @description The ID of history from which the object should be downloaded - * @example 0123456789ABCDEF - */ - history_id: string; - /** - * Spaces to tabs - * @description A boolean value. If set to 'True', and an object with same name of the dataset to be sent already exist in the bucket, Galaxy replaces the existing object with the dataset to be sent. If set to 'False', Galaxy appends datetime to the dataset name to prevent overwriting an existing object. - * @default false - */ - overwrite_existing?: boolean | null; - }; - /** CloudDatasetsResponse */ - CloudDatasetsResponse: { - /** - * Bucket - * @description The name of bucket to which the listed datasets are queued to be sent - */ - bucket_name: string; - /** - * Failed datasets - * @description The datasets for which Galaxy failed to create (and queue) send job - */ - failed_dataset_labels: string[]; - /** - * Send datasets - * @description The datasets for which Galaxy succeeded to create (and queue) send job - */ - sent_dataset_labels: string[]; - }; - /** CloudObjects */ - CloudObjects: { - /** - * Authentication ID - * @description The ID of CloudAuthz to be used for authorizing access to the resource provider. You may get a list of the defined authorizations via `/api/cloud/authz`. Also, you can use `/api/cloud/authz/create` to define a new authorization. - * @example 0123456789ABCDEF - */ - authz_id: string; - /** - * Bucket - * @description The name of a bucket from which data should be fetched from (e.g., a bucket name on AWS S3). - */ - bucket: string; - /** - * History ID - * @description The ID of history to which the object should be received to. - * @example 0123456789ABCDEF - */ - history_id: string; - /** - * Input arguments - * @description A summary of the input arguments, which is optional and will default to {}. - */ - input_args?: components["schemas"]["InputArguments"] | null; - /** - * Objects - * @description A list of the names of objects to be fetched. - */ - objects: string[]; - }; /** CollectionElementIdentifier */ CollectionElementIdentifier: { /** @@ -4192,47 +4099,6 @@ export interface components { */ sources: Record[]; }; - /** DatasetSummary */ - DatasetSummary: { - /** - * Create Time - * @description The time and date this item was created. - */ - create_time: string | null; - /** Deleted */ - deleted: boolean; - /** File Size */ - file_size: number; - /** - * Id - * @example 0123456789ABCDEF - */ - id: string; - /** Purgable */ - purgable: boolean; - /** Purged */ - purged: boolean; - /** - * State - * @description The current state of this dataset. - */ - state: components["schemas"]["DatasetState"]; - /** Total Size */ - total_size: number; - /** - * Update Time - * @description The last time and date this item was updated. - */ - update_time: string | null; - /** - * UUID - * Format: uuid4 - * @description Universal unique identifier for this dataset. - */ - uuid: string; - }; - /** DatasetSummaryList */ - DatasetSummaryList: components["schemas"]["DatasetSummary"][]; /** DatasetTextContentDetails */ DatasetTextContentDetails: { /** @@ -7377,33 +7243,6 @@ export interface components { */ uri: string; }; - /** InputArguments */ - InputArguments: { - /** - * Database Key - * @description Sets the database key of the objects being fetched to Galaxy. - * @default ? - */ - dbkey?: string | null; - /** - * File Type - * @description Sets the Galaxy datatype (e.g., `bam`) for the objects being fetched to Galaxy. See the following link for a complete list of Galaxy data types: https://galaxyproject.org/learn/datatypes/. - * @default auto - */ - file_type?: string | null; - /** - * Spaces to tabs - * @description A boolean value ('true' or 'false') that sets if spaces should be converted to tab in the objects being fetched to Galaxy. Applicable only if `to_posix_lines` is True - * @default false - */ - space_to_tab?: boolean | null; - /** - * POSIX line endings - * @description A boolean value ('true' or 'false'); if 'Yes', converts universal line endings to POSIX line endings. Set to 'False' if you upload a gzip, bz2 or zip archive containing a binary file. - * @default Yes - */ - to_posix_lines?: "Yes" | boolean | null; - }; /** InputDataCollectionStep */ InputDataCollectionStep: { /** @@ -13453,68 +13292,6 @@ export interface operations { }; }; }; - get_api_cloud_storage_get_post: { - /** - * Gets given objects from a given cloud-based bucket to a Galaxy history. - * @deprecated - */ - parameters?: { - /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ - header?: { - "run-as"?: string | null; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CloudObjects"]; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - content: { - "application/json": components["schemas"]["DatasetSummaryList"]; - }; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - send_api_cloud_storage_send_post: { - /** - * Sends given dataset(s) in a given history to a given cloud-based bucket. - * @deprecated - */ - parameters?: { - /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ - header?: { - "run-as"?: string | null; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CloudDatasets"]; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - content: { - "application/json": components["schemas"]["CloudDatasetsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; index_api_configuration_get: { /** * Return an object containing exposable configuration settings diff --git a/client/src/components/User/CloudAuth/CloudAuth.test.js b/client/src/components/User/CloudAuth/CloudAuth.test.js deleted file mode 100644 index 705a304b5dde..000000000000 --- a/client/src/components/User/CloudAuth/CloudAuth.test.js +++ /dev/null @@ -1,72 +0,0 @@ -import { shallowMount } from "@vue/test-utils"; -import flushPromises from "flush-promises"; -import { getLocalVue } from "tests/jest/helpers"; - -import { default as CloudAuth } from "./CloudAuth"; -import CloudAuthItem from "./CloudAuthItem"; - -jest.mock("./model/service", () => ({ - listCredentials: async () => { - const listCredentials = require("./testdata/listCredentials.json"); - const Credential = require("./model").Credential; - return listCredentials.map(Credential.create); - }, -})); - -const localVue = getLocalVue(); - -describe("CloudAuth component", () => { - let wrapper; - - beforeEach(async () => { - wrapper = shallowMount(CloudAuth, { localVue }); - await flushPromises(); - }); - - describe("initialization", () => { - it("should render the initial list", () => { - expect(wrapper).toBeTruthy(); - expect(wrapper.findComponent(CloudAuthItem).exists()).toBeTruthy(); - expect(wrapper.vm.items.length == 2).toBeTruthy(); - expect(wrapper.vm.filteredItems.length == 2).toBeTruthy(); - }); - }); - - describe("text filter", () => { - it("should show filter result by text match", () => { - let results; - - wrapper.vm.filter = "aws"; - results = wrapper.vm.filteredItems; - expect(wrapper.findComponent(CloudAuthItem).exists()).toBeTruthy(); - expect(results.length == 1).toBeTruthy(); - - wrapper.vm.filter = "azure"; - results = wrapper.vm.filteredItems; - expect(results.length == 1).toBeTruthy(); - - wrapper.vm.filter = ""; - results = wrapper.vm.filteredItems; - expect(results.length == 2).toBeTruthy(); - }); - }); - - describe("create button", () => { - it("clicking create button should add a blank key", () => { - let results = wrapper.vm.filteredItems; - expect(wrapper.findComponent(CloudAuthItem).exists()).toBeTruthy(); - expect(results.length == 2).toBeTruthy(); - - const button = wrapper.find("button[name=createNewKey]"); - expect(button).toBeTruthy(); - button.trigger("click"); - - results = wrapper.vm.filteredItems; - expect(results.length == 3).toBeTruthy(); - - const blank = results.find((i) => i.id == null); - expect(blank).toBeTruthy(); - expect(blank.id == null).toBeTruthy(); - }); - }); -}); diff --git a/client/src/components/User/CloudAuth/CloudAuth.vue b/client/src/components/User/CloudAuth/CloudAuth.vue deleted file mode 100644 index 0a00e1d3c71a..000000000000 --- a/client/src/components/User/CloudAuth/CloudAuth.vue +++ /dev/null @@ -1,372 +0,0 @@ - - - - - diff --git a/client/src/components/User/CloudAuth/CloudAuthItem.vue b/client/src/components/User/CloudAuth/CloudAuthItem.vue deleted file mode 100644 index 8c5526c3d0b3..000000000000 --- a/client/src/components/User/CloudAuth/CloudAuthItem.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - diff --git a/client/src/components/User/CloudAuth/CredentialConfig.vue b/client/src/components/User/CloudAuth/CredentialConfig.vue deleted file mode 100644 index f9faaeef7b3e..000000000000 --- a/client/src/components/User/CloudAuth/CredentialConfig.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - diff --git a/client/src/components/User/CloudAuth/CredentialForm.vue b/client/src/components/User/CloudAuth/CredentialForm.vue deleted file mode 100644 index 4c44c6fe6926..000000000000 --- a/client/src/components/User/CloudAuth/CredentialForm.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - diff --git a/client/src/components/User/CloudAuth/index.js b/client/src/components/User/CloudAuth/index.js deleted file mode 100644 index 3e5bd9184df0..000000000000 --- a/client/src/components/User/CloudAuth/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as CloudAuth } from "./CloudAuth.vue"; diff --git a/client/src/components/User/CloudAuth/model/AwsConfig.js b/client/src/components/User/CloudAuth/model/AwsConfig.js deleted file mode 100644 index 9c4f27164d69..000000000000 --- a/client/src/components/User/CloudAuth/model/AwsConfig.js +++ /dev/null @@ -1,28 +0,0 @@ -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) { - const 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/src/components/User/CloudAuth/model/AwsConfig.test.js b/client/src/components/User/CloudAuth/model/AwsConfig.test.js deleted file mode 100644 index 938fd1974e66..000000000000 --- a/client/src/components/User/CloudAuth/model/AwsConfig.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import { AwsConfig } from "./AwsConfig"; - -describe("AwsConfig", () => { - it("should instantiate", () => { - const instance = new AwsConfig(); - expect(instance).toBeTruthy(); - expect(!instance.dirty).toBeTruthy(); - expect(!instance.valid).toBeTruthy(); - }); - - it("should validate role_arn", () => { - const instance = new AwsConfig(); - instance.role_arn = "abc"; - expect(instance.fieldValid("role_arn")).toBeTruthy(); - }); - - it("should invalidate role_arn", () => { - const instance = new AwsConfig(); - instance.role_arn = ""; - expect(!instance.fieldValid("role_arn")).toBeTruthy(); - }); -}); diff --git a/client/src/components/User/CloudAuth/model/AzureConfig.js b/client/src/components/User/CloudAuth/model/AzureConfig.js deleted file mode 100644 index 800dd7ea3344..000000000000 --- a/client/src/components/User/CloudAuth/model/AzureConfig.js +++ /dev/null @@ -1,62 +0,0 @@ -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) { - const 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/src/components/User/CloudAuth/model/AzureConfig.test.js b/client/src/components/User/CloudAuth/model/AzureConfig.test.js deleted file mode 100644 index 35c4c9665937..000000000000 --- a/client/src/components/User/CloudAuth/model/AzureConfig.test.js +++ /dev/null @@ -1,58 +0,0 @@ -import { AzureConfig } from "./AzureConfig"; - -describe("AzureConfig", () => { - it("should instantiate", () => { - const instance = new AzureConfig(); - expect(instance).toBeTruthy(); - expect(!instance.dirty).toBeTruthy(); - expect(!instance.valid).toBeTruthy(); - }); - - describe("client_secret", () => { - it("should validate client_secret", () => { - const instance = new AzureConfig(); - instance.client_secret = "abc"; - expect(instance.fieldValid("client_secret")).toBeTruthy(); - }); - - it("should invalidate client_secret", () => { - const instance = new AzureConfig(); - instance.client_secret = ""; - expect(!instance.fieldValid("client_secret")).toBeTruthy(); - }); - }); - - describe("tenant_id", () => { - it("should validate tenant_id", () => { - const validTenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; - const instance = new AzureConfig(); - instance.tenant_id = validTenantId; - expect(instance.fieldValid("tenant_id")).toBeTruthy(); - }); - - it("should invalidate tenant_id", () => { - const instance = new AzureConfig(); - instance.tenant_id = ""; - expect(!instance.fieldValid("tenant_id")).toBeTruthy(); - instance.tenant_id = "asdfa"; - expect(!instance.fieldValid("tenant_id")).toBeTruthy(); - }); - }); - - describe("client_id", () => { - it("should validate client_id", () => { - const validClientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; - const instance = new AzureConfig(); - instance.client_id = validClientId; - expect(instance.fieldValid("client_id")).toBeTruthy(); - }); - - it("should invalidate client_id", () => { - const instance = new AzureConfig(); - instance.client_id = ""; - expect(!instance.fieldValid("client_id")).toBeTruthy(); - instance.client_id = "asdf"; - expect(!instance.fieldValid("client_id")).toBeTruthy(); - }); - }); -}); diff --git a/client/src/components/User/CloudAuth/model/BaseModel.js b/client/src/components/User/CloudAuth/model/BaseModel.js deleted file mode 100644 index 5dede7517def..000000000000 --- a/client/src/components/User/CloudAuth/model/BaseModel.js +++ /dev/null @@ -1,102 +0,0 @@ -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 - const 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() { - const 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) { - const klass = this; // this will be a class - if (!transients.has(klass)) { - transients.set(klass, new Set()); - } - - const 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)) { - const validator = validators.get(this); - return validator(model); - } - throw new Error("Missing validator"); - } - - static setValidator(validationFunction) { - validators.set(this, validationFunction); - } -} diff --git a/client/src/components/User/CloudAuth/model/Credential.js b/client/src/components/User/CloudAuth/model/Credential.js deleted file mode 100644 index 6e5470d2bd0b..000000000000 --- a/client/src/components/User/CloudAuth/model/Credential.js +++ /dev/null @@ -1,105 +0,0 @@ -import { safeAssign } from "utils/safeAssign"; - -import { BaseModel } from "./BaseModel"; -import { ResourceProviders } from "./ResourceProviders"; - -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 - const 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 - - match(searchText = "") { - // TODO: more robust object matching? - 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) { - const 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; -}); diff --git a/client/src/components/User/CloudAuth/model/Credential.test.js b/client/src/components/User/CloudAuth/model/Credential.test.js deleted file mode 100644 index 91978505d946..000000000000 --- a/client/src/components/User/CloudAuth/model/Credential.test.js +++ /dev/null @@ -1,106 +0,0 @@ -import { Credential } from "./Credential"; - -describe("Credential model", () => { - describe("basic model props", () => { - it("should exist", () => { - expect(Credential).toBeTruthy(); - }); - - it("should build with defaults", () => { - const instance = new Credential(); - expect(instance).toBeTruthy(); - }); - - it("should build with props", () => { - const description = "i am the test description"; - const props = { description }; - const instance = new Credential(props); - expect(instance).toBeTruthy(); - expect(instance.description == description).toBeTruthy(); - }); - - it("default config object should be AWS", () => { - const instance = new Credential(); - expect("role_arn" in instance.config).toBeTruthy(); - expect(instance.config.constructor.name == "AwsConfig").toBeTruthy(); - }); - - it("should load a different config object given the right provider", () => { - const provider = "azure"; - const props = { provider }; - const instance = new Credential(props); - expect(instance.provider == provider).toBeTruthy(); - expect(instance.config.constructor.name == "AzureConfig").toBeTruthy(); - }); - - it("should dynamically switch config objects as resourceProvider is changed", () => { - const instance = new Credential(); - expect(instance.provider == "aws").toBeTruthy(); - instance.resourceProvider = "azure"; - expect(instance.config.constructor.name == "AzureConfig").toBeTruthy(); - instance.resourceProvider = "aws"; - expect(instance.config.constructor.name == "AwsConfig").toBeTruthy(); - }); - }); - - describe("dirty state", () => { - it("should flag as dirty when a prop is changed", () => { - const instance = new Credential(); - instance.description = "foo"; - expect(instance.dirty).toBeTruthy(); - }); - - it("should flag as clean when a prop is restored", () => { - const instance = new Credential(); - instance.description = "foo"; - expect(instance.dirty).toBeTruthy(); - instance.description = ""; - expect(!instance.dirty).toBeTruthy(); - }); - - it("should not flag as dirty when a transient prop is changed", () => { - const instance = new Credential(); - instance.loading = true; - expect(!instance.dirty).toBeTruthy(); - }); - }); - - describe("validation", () => { - it("a new object should be invalid", () => { - const instance = new Credential(); - expect(!instance.valid).toBeTruthy(); - }); - - it("it should become valid when props are assigned", () => { - const instance = new Credential(); - instance.authn_id = "floob"; - instance.resourceProvider = "aws"; - instance.config.role_arn = "abc"; - expect(instance.valid).toBeTruthy(); - }); - - it("it should be valid when initialized with correct props", () => { - const props = { - authn_id: "asdfasdf", - provider: "aws", - config: { - role_arn: "floobar", - }, - }; - const instance = new Credential(props); - expect(instance.valid).toBeTruthy(); - }); - - it("should be invalid when bad props assigned", () => { - const props = { - authn_id: "asdfasdf", - provider: "aws", - config: { - role_arn: "", - }, - }; - const instance = new Credential(props); - expect(!instance.valid).toBeTruthy(); - }); - }); -}); diff --git a/client/src/components/User/CloudAuth/model/IdentityProvider.js b/client/src/components/User/CloudAuth/model/IdentityProvider.js deleted file mode 100644 index 7c0974fc50b0..000000000000 --- a/client/src/components/User/CloudAuth/model/IdentityProvider.js +++ /dev/null @@ -1,29 +0,0 @@ -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/src/components/User/CloudAuth/model/ResourceProviders.js b/client/src/components/User/CloudAuth/model/ResourceProviders.js deleted file mode 100644 index cefcbb63faa6..000000000000 --- a/client/src/components/User/CloudAuth/model/ResourceProviders.js +++ /dev/null @@ -1,14 +0,0 @@ -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/src/components/User/CloudAuth/model/index.js b/client/src/components/User/CloudAuth/model/index.js deleted file mode 100644 index 24f019b67c53..000000000000 --- a/client/src/components/User/CloudAuth/model/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { Credential } from "./Credential"; -export { IdentityProvider } from "./IdentityProvider"; -export { ResourceProviders } from "./ResourceProviders"; diff --git a/client/src/components/User/CloudAuth/model/service.js b/client/src/components/User/CloudAuth/model/service.js deleted file mode 100644 index e5cdb03a66d9..000000000000 --- a/client/src/components/User/CloudAuth/model/service.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Data retrieval/storage for the auth keys - */ - -import axios from "axios"; -import { getRootFromIndexLink } from "onload"; - -import { Credential, IdentityProvider } from "./index"; - -const getUrl = (path) => getRootFromIndexLink() + path; - -export async function listCredentials() { - const url = getUrl("api/cloud/authz"); - const response = await axios.get(url); - if (response.status != 200) { - throw new Error("Unexpected response from listing."); - } - return response.data.map(Credential.create); -} - -export async function getCredential(id) { - const url = getUrl("api/cloud/authz/${id}"); - const response = await axios.get(url); - if (response.status != 200) { - throw new Error("Unexpected response loading key."); - } - return Credential.create(response.data); -} - -export async function saveCredential(newItem) { - const model = Credential.create(newItem); - const response = await saveOrUpdate(model); - if (response.status != 200) { - throw new Error("Save failure."); - } - 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) { - const model = Credential.create(doomed); - if (model.id) { - const url = getUrl(`api/cloud/authz/${doomed.id}`); - const response = await axios.delete(url); - if (response.status != 200) { - throw new Error("Delete failure."); - } - } - return model; -} - -// Memoize results (basically never changes) - -let identityProviders; - -export async function getIdentityProviders() { - if (!identityProviders) { - const url = getUrl("authnz"); - const response = await axios.get(url); - if (response.status != 200) { - throw new Error("Unable to load identity providers"); - } - // This should be idempotent (and safe). - // eslint-disable-next-line require-atomic-updates - identityProviders = response.data.map(IdentityProvider.create); - } - return identityProviders; -} - -export default { - listCredentials, - getCredential, - saveCredential, - deleteCredential, - getIdentityProviders, -}; diff --git a/client/src/components/User/CloudAuth/testdata/listCredentials.json b/client/src/components/User/CloudAuth/testdata/listCredentials.json deleted file mode 100644 index 82973dc3a7fc..000000000000 --- a/client/src/components/User/CloudAuth/testdata/listCredentials.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "authn_id": "f2db41e1fa331b3e", - "user_id": "f2db41e1fa331b3e", - "description": "aws description", - "last_update": "2019-04-04 09:13:16.135032", - "last_activity": "2019-04-04 09:13:16.135042", - "create_time": "2019-04-04 16:13:16.135755", - "provider": "aws", - "model_class": "CloudAuthz", - "config": { - "role_arn": "floobadooba" - }, - "id": "a7db2fac67043c7e" - }, - { - "authn_id": "f2db41e1fa331b3e", - "user_id": "f2db41e1fa331b3e", - "description": "azure description", - "last_update": "2019-04-03 20:21:51.350748", - "last_activity": "2019-04-03 20:21:51.350804", - "create_time": "2019-04-04 03:21:51.351508", - "provider": "azure", - "model_class": "CloudAuthz", - "config": { - "client_secret": "dannon likes Nickleback", - "client_id": "22222222-2222-2222-2222-222222222222", - "tenant_id": "11111111-1111-1111-1111-111111111111" - }, - "id": "b472e2eb553fa0d1" - } -] \ No newline at end of file diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index 89f7d058a7ab..8f5772ea37c1 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -24,7 +24,6 @@ import ToolsJson from "components/ToolsView/ToolsSchemaJson/ToolsJson"; import TourList from "components/Tour/TourList"; import TourRunner from "components/Tour/TourRunner"; import { APIKey } from "components/User/APIKey"; -import { CloudAuth } from "components/User/CloudAuth"; import CustomBuilds from "components/User/CustomBuilds"; import { ExternalIdentities } from "components/User/ExternalIdentities"; import { NotificationsPreferences } from "components/User/Notifications"; @@ -499,11 +498,6 @@ export function getRouter(Galaxy) { component: APIKey, redirect: redirectAnon(), }, - { - path: "user/cloud_auth", - component: CloudAuth, - redirect: redirectAnon(), - }, { path: "user/external_ids", component: ExternalIdentities, diff --git a/lib/galaxy/authnz/managers.py b/lib/galaxy/authnz/managers.py index fe030be0b341..93fc3c010ac3 100644 --- a/lib/galaxy/authnz/managers.py +++ b/lib/galaxy/authnz/managers.py @@ -1,13 +1,5 @@ import builtins -import copy -import json import logging -import os -import random -import string - -from cloudauthz import CloudAuthz -from cloudauthz.exceptions import CloudAuthzBaseException from galaxy import ( exceptions, @@ -18,7 +10,6 @@ etree, listify, parse_xml, - requests, string_as_bool, unicodify, ) @@ -32,10 +23,7 @@ ) from .psa_authnz import ( BACKENDS_NAME, - on_the_fly_config, PSAAuthnz, - Storage, - Strategy, ) OIDC_BACKEND_SCHEMA = resource_path(__package__, "xsd/oidc_backends_config.xsd") @@ -263,35 +251,6 @@ def _get_identity_provider_factory(implementation): else: return None - def _extend_cloudauthz_config(self, cloudauthz, request, sa_session, user_id): - config = copy.deepcopy(cloudauthz.config) - if cloudauthz.provider == "aws": - success, message, backend = self._get_authnz_backend(cloudauthz.authn.provider) - strategy = Strategy(request, None, Storage, backend.config) - on_the_fly_config(sa_session) - try: - config["id_token"] = cloudauthz.authn.get_id_token(strategy) - except requests.exceptions.HTTPError as e: - msg = ( - f"Sign-out from Galaxy and remove its access from `{self._unify_provider_name(cloudauthz.authn.provider)}`, " - "then log back in using `{cloudauthz.authn.uid}` account." - ) - log.debug( - "Failed to get/refresh ID token for user with ID `%s` for assuming authz_id `%s`. " - "User may not have a refresh token. If the problem persists, set the `prompt` key to " - "`consent` in `oidc_backends_config.xml`, then restart Galaxy and ask user to: %s" - "Error Message: `%s`", - user_id, - cloudauthz.id, - msg, - e.response.text, - ) - raise exceptions.AuthenticationFailed( - err_msg=f"An error occurred getting your ID token. {msg}. If the problem persists, please " - "contact Galaxy admin." - ) - return config - @staticmethod def can_user_assume_authn(trans, authn_id): qres = trans.sa_session.query(model.UserAuthnzToken).get(authn_id) @@ -307,36 +266,6 @@ def can_user_assume_authn(trans, authn_id): log.warning(msg) raise exceptions.ItemAccessibilityException(msg) - @staticmethod - def try_get_authz_config(sa_session, user_id, authz_id): - """ - It returns a cloudauthz config (see model.CloudAuthz) with the - given ID; and raise an exception if either a config with given - ID does not exist, or the configuration is defined for a another - user than trans.user. - - :type trans: galaxy.webapps.base.webapp.GalaxyWebTransaction - :param trans: Galaxy web transaction - - :type authz_id: int - :param authz_id: The ID of a CloudAuthz configuration to be used for - getting temporary credentials. - - :rtype : model.CloudAuthz - :return: a cloudauthz configuration. - """ - qres = sa_session.query(model.CloudAuthz).get(authz_id) - if qres is None: - raise exceptions.ObjectNotFound("An authorization configuration with given ID not found.") - if user_id != qres.user_id: - msg = ( - f"The request authorization configuration (with ID:`{qres.id}`) is not accessible for user with " - f"ID:`{user_id}`." - ) - log.warning(msg) - raise exceptions.ItemAccessibilityException(msg) - return qres - def refresh_expiring_oidc_tokens_for_provider(self, trans, auth): try: success, message, backend = self._get_authnz_backend(auth.provider) @@ -509,98 +438,3 @@ def disconnect(self, provider, trans, email=None, disconnect_redirect_url=None, msg = f"An error occurred when disconnecting authentication with `{provider}` identity provider for user `{trans.user.username}`" log.exception(msg) return False, msg, None - - def get_cloud_access_credentials(self, cloudauthz, sa_session, user_id, request=None): - """ - This method leverages CloudAuthz (https://github.com/galaxyproject/cloudauthz) - to request a cloud-based resource provider (e.g., Amazon AWS, Microsoft Azure) - for temporary access credentials to a given resource. - - It first checks if a cloudauthz config with the given ID (`authz_id`) is - available and can be assumed by the user, and raises an exception if either - is false. Otherwise, it then extends the cloudauthz configuration as required - by the CloudAuthz library for the provider specified in the configuration. - For instance, it adds on-the-fly values such as a valid OpenID Connect - identity token, as required by CloudAuthz for AWS. Then requests temporary - credentials from the CloudAuthz library using the updated configuration. - - :type cloudauthz: CloudAuthz - :param cloudauthz: an instance of CloudAuthz to be used for getting temporary - credentials. - - :type sa_session: sqlalchemy.orm.scoping.scoped_session - :param sa_session: SQLAlchemy database handle. - - :type user_id: int - :param user_id: Decoded Galaxy user ID. - - :type request: galaxy.web.framework.base.Request - :param request: Encapsulated HTTP(S) request. - - :rtype: dict - :return: a dictionary containing credentials to access a cloud-based - resource provider. See CloudAuthz (https://github.com/galaxyproject/cloudauthz) - for details on the content of this dictionary. - """ - config = self._extend_cloudauthz_config(cloudauthz, request, sa_session, user_id) - try: - ca = CloudAuthz() - log.info( - "Requesting credentials using CloudAuthz with config id `%s` on be half of user `%s`.", - cloudauthz.id, - user_id, - ) - credentials = ca.authorize(cloudauthz.provider, config) - return credentials - except CloudAuthzBaseException as e: - log.info(e) - raise exceptions.AuthenticationFailed(e) - except NotImplementedError as e: - log.info(e) - raise exceptions.RequestParameterInvalidException(e) - - def get_cloud_access_credentials_in_file(self, new_file_path, cloudauthz, sa_session, user_id, request=None): - """ - This method leverages CloudAuthz (https://github.com/galaxyproject/cloudauthz) - to request a cloud-based resource provider (e.g., Amazon AWS, Microsoft Azure) - for temporary access credentials to a given resource. - - This method uses the `get_cloud_access_credentials` method to obtain temporary - credentials, and persists them to a (temporary) file, and returns the file path. - - :type new_file_path: str - :param new_file_path: Where dataset files are saved on temporary storage. - See `app.config.new_file_path`. - - :type cloudauthz: CloudAuthz - :param cloudauthz: an instance of CloudAuthz to be used for getting temporary - credentials. - - :type sa_session: sqlalchemy.orm.scoping.scoped_session - :param sa_session: SQLAlchemy database handle. - - :type user_id: int - :param user_id: Decoded Galaxy user ID. - - :type request: galaxy.web.framework.base.Request - :param request: [Optional] Encapsulated HTTP(S) request. - - :rtype: str - :return: The filename to which credentials are written. - """ - filename = os.path.abspath( - os.path.join( - new_file_path, - "cd_" - + "".join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(11)), - ) - ) - credentials = self.get_cloud_access_credentials(cloudauthz, sa_session, user_id, request) - log.info( - "Writing credentials generated using CloudAuthz with config id `%s` to the following file: `%s`", - cloudauthz.id, - filename, - ) - with open(filename, "w") as f: - f.write(json.dumps(credentials)) - return filename diff --git a/lib/galaxy/managers/cloud.py b/lib/galaxy/managers/cloud.py deleted file mode 100644 index 813b5690b7ee..000000000000 --- a/lib/galaxy/managers/cloud.py +++ /dev/null @@ -1,400 +0,0 @@ -""" -Manager and serializer for cloud-based storages. -""" - -import json -import logging - -from galaxy import ( - model, - util, -) -from galaxy.exceptions import ( - ItemAccessibilityException, - MessageException, - ObjectNotFound, - RequestParameterInvalidException, - RequestParameterMissingException, -) -from galaxy.managers import sharable -from galaxy.util import Params - -try: - from cloudbridge.factory import ( - CloudProviderFactory, - ProviderList, - ) -except ImportError: - CloudProviderFactory = None - ProviderList = None - -log = logging.getLogger(__name__) - -NO_CLOUDBRIDGE_ERROR_MESSAGE = ( - "Cloud ObjectStore is configured, but no CloudBridge dependency available." - "Please install CloudBridge or modify ObjectStore configuration." -) - -# Any change to this list, MUST be reflected in the SEND_TOOL wrapper -# (tools/cloud/send.xml). -SUPPORTED_PROVIDERS = {"aws": 0, "azure": 1} - -SEND_TOOL = "send_to_cloud" -SEND_TOOL_VERSION = "0.1.0" - -# TODO: this configuration should be set in a config file. -SINGED_URL_TTL = 3600 - - -class CloudManager(sharable.SharableModelManager): - # This manager does not manage a history; however, - # some of its functions require operations - # on history objects using methods from the base - # manager class (e.g., get_accessible), which requires - # setting this property. - model_class = model.History - - @staticmethod - def configure_provider(provider, credentials): - """ - Given a provider name and required credentials, it configures and returns a cloudbridge - connection to the provider. - - :type provider: string - :param provider: the name of cloud-based resource provided. A list of supported providers is given in - `SUPPORTED_PROVIDERS` variable. - - :type credentials: dict - :param credentials: a dictionary containing all the credentials required to authenticated to the - specified provider. - - :rtype: provider specific, e.g., `cloudbridge.cloud.providers.aws.provider.AWSCloudProvider` for AWS. - :return: a cloudbridge connection to the specified provider. - """ - missing_credentials = [] - if provider == "aws": - access = credentials.get("access_key", None) - if access is None: - access = credentials.get("AccessKeyId", None) - if access is None: - missing_credentials.append("access_key") - secret = credentials.get("secret_key", None) - if secret is None: - secret = credentials.get("SecretAccessKey", None) - if secret is None: - missing_credentials.append("secret_key") - if len(missing_credentials) > 0: - raise RequestParameterMissingException( - "The following required key(s) are missing from the provided " - f"credentials object: {missing_credentials}" - ) - session_token = credentials.get("SessionToken") - config = {"aws_access_key": access, "aws_secret_key": secret, "aws_session_token": session_token} - connection = CloudProviderFactory().create_provider(ProviderList.AWS, config) - elif provider == "azure": - subscription = credentials.get("subscription_id", None) - if subscription is None: - missing_credentials.append("subscription_id") - client = credentials.get("client_id", None) - if client is None: - missing_credentials.append("client_id") - secret = credentials.get("secret", None) - if secret is None: - missing_credentials.append("secret") - tenant = credentials.get("tenant", None) - if tenant is None: - missing_credentials.append("tenant") - if len(missing_credentials) > 0: - raise RequestParameterMissingException( - "The following required key(s) are missing from the provided " - f"credentials object: {missing_credentials}" - ) - - config = { - "azure_subscription_id": subscription, - "azure_client_id": client, - "azure_secret": secret, - "azure_tenant": tenant, - } - storage_account = credentials.get("storage_account") - if storage_account: - config["azure_storage_account"] = storage_account - resource_group = credentials.get("resource_group") - if resource_group: - config["azure_resource_group"] = resource_group - connection = CloudProviderFactory().create_provider(ProviderList.AZURE, config) - elif provider == "openstack": - username = credentials.get("username", None) - if username is None: - missing_credentials.append("username") - password = credentials.get("password", None) - if password is None: - missing_credentials.append("password") - auth_url = credentials.get("auth_url", None) - if auth_url is None: - missing_credentials.append("auth_url") - prj_name = credentials.get("project_name", None) - if prj_name is None: - missing_credentials.append("project_name") - prj_domain_name = credentials.get("project_domain_name", None) - if prj_domain_name is None: - missing_credentials.append("project_domain_name") - user_domain_name = credentials.get("user_domain_name", None) - if user_domain_name is None: - missing_credentials.append("user_domain_name") - if len(missing_credentials) > 0: - raise RequestParameterMissingException( - "The following required key(s) are missing from the provided " - f"credentials object: {missing_credentials}" - ) - config = { - "os_username": username, - "os_password": password, - "os_auth_url": auth_url, - "os_project_name": prj_name, - "os_project_domain_name": prj_domain_name, - "os_user_domain_name": user_domain_name, - } - connection = CloudProviderFactory().create_provider(ProviderList.OPENSTACK, config) - elif provider == "gcp": - config = {"gcp_service_creds_dict": credentials} - connection = CloudProviderFactory().create_provider(ProviderList.GCP, config) - else: - raise RequestParameterInvalidException( - f"Unrecognized provider '{provider}'; the following are the supported " - f"providers: {SUPPORTED_PROVIDERS.keys()}." - ) - - # The authorization-assertion mechanism of Cloudbridge assumes a user has an elevated privileges, - # such as Admin-level access to all resources (see https://github.com/CloudVE/cloudbridge/issues/135). - # As a result, a user who wants to authorize Galaxy to read/write an Amazon S3 bucket, need to - # also authorize Galaxy with full permission to Amazon EC2 (because Cloudbridge leverages EC2-specific - # operation to assert credentials). While the EC2 authorization is not required by Galaxy to - # read/write a S3 bucket, it can cause this exception. - # - # Until Cloudbridge implements an authorization-specific credentials assertion, we are not asserting - # the authorization/validity of the credentials, in order to avoid asking users to grant Galaxy with an - # elevated, yet unnecessary, privileges. - # - # Note, if user's credentials are invalid/expired to perform the authorized action, that can cause - # exceptions which we capture separately in related read/write attempts. - return connection - - @staticmethod - def _get_inputs(obj, key, input_args): - space_to_tab = None - if input_args.get("space_to_tab", "").lower() == "true": - space_to_tab = "Yes" - elif input_args.get("space_to_tab", "").lower() not in ["false", ""]: - raise RequestParameterInvalidException( - "The valid values for `space_to_tab` argument are `true` and `false`; received {}".format( - input_args.get("space_to_tab") - ) - ) - - to_posix_lines = None - if input_args.get("to_posix_lines", "").lower() == "true": - to_posix_lines = "Yes" - elif input_args.get("to_posix_lines", "").lower() not in ["false", ""]: - raise RequestParameterInvalidException( - "The valid values for `to_posix_lines` argument are `true` and `false`; received {}".format( - input_args.get("to_posix_lines") - ) - ) - - return { - "dbkey": input_args.get("dbkey", "?"), - "file_type": input_args.get("file_type", "auto"), - "files_0|type": "upload_dataset", - "files_0|space_to_tab": space_to_tab, - "files_0|to_posix_lines": to_posix_lines, - "files_0|NAME": obj, - "files_0|url_paste": key.generate_url(expires_in=SINGED_URL_TTL), - } - - def get(self, trans, history_id, bucket_name, objects, authz_id, input_args=None): - """ - Implements the logic of getting a file from a cloud-based storage (e.g., Amazon S3) - and persisting it as a Galaxy dataset. - - This manager does NOT require use credentials, instead, it uses a more secure method, - which leverages CloudAuthz (https://github.com/galaxyproject/cloudauthz) and automatically - requests temporary credentials to access the defined resources. - - :type trans: galaxy.webapps.base.webapp.GalaxyWebTransaction - :param trans: Galaxy web transaction - - :type history_id: string - :param history_id: the (decoded) id of history to which the object should be received to. - - :type bucket_name: string - :param bucket_name: the name of a bucket from which data should be fetched (e.g., a bucket name on AWS S3). - - :type objects: list of string - :param objects: the name of objects to be fetched. - - :type authz_id: int - :param authz_id: the ID of CloudAuthz to be used for authorizing access to the resource provider. You may - get a list of the defined authorizations sending GET to `/api/cloud/authz`. Also, you can - POST to `/api/cloud/authz` to define a new authorization. - - :type input_args: dict - :param input_args: a [Optional] a dictionary of input parameters: - dbkey, file_type, space_to_tab, to_posix_lines (see galaxy/webapps/galaxy/api/cloud.py) - - :rtype: list of galaxy.model.Dataset - :return: a list of datasets created for the fetched files. - """ - if CloudProviderFactory is None: - raise Exception(NO_CLOUDBRIDGE_ERROR_MESSAGE) - - if input_args is None: - input_args = {} - - if not hasattr(trans.app, "authnz_manager"): - err_msg = ( - "The OpenID Connect protocol, a required feature for getting data from cloud, " - "is not enabled on this Galaxy instance." - ) - log.debug(err_msg) - raise MessageException(err_msg) - - cloudauthz = trans.app.authnz_manager.try_get_authz_config(trans.sa_session, trans.user.id, authz_id) - credentials = trans.app.authnz_manager.get_cloud_access_credentials( - cloudauthz, trans.sa_session, trans.user.id, trans.request - ) - connection = self.configure_provider(cloudauthz.provider, credentials) - try: - bucket = connection.storage.buckets.get(bucket_name) - if bucket is None: - raise RequestParameterInvalidException(f"The bucket `{bucket_name}` not found.") - except Exception as e: - raise ItemAccessibilityException(f"Could not get the bucket `{bucket_name}`: {util.unicodify(e)}") - - datasets = [] - for obj in objects: - try: - key = bucket.objects.get(obj) - except Exception as e: - raise MessageException( - f"The following error occurred while getting the object {obj}: {util.unicodify(e)}" - ) - if key is None: - log.exception( - "Could not get object `%s` for user `%s`. Object may not exist, or the provided credentials are " - "invalid or not authorized to read the bucket/object.", - obj, - trans.user.id, - ) - raise ObjectNotFound( - f"Could not get the object `{obj}`. Please check if the object exists, and credentials are valid and " - "authorized to read the bucket and object. " - ) - - params = Params(self._get_inputs(obj, key, input_args), sanitize=False) - incoming = params.__dict__ - history = trans.sa_session.get(model.History, history_id) - if not history: - raise ObjectNotFound("History with the ID provided was not found.") - output = trans.app.toolbox.get_tool("upload1").handle_input(trans, incoming, history=history) - - job_errors = output.get("job_errors", []) - if job_errors: - raise ValueError( - f"Following error occurred while getting the given object(s) from {cloudauthz.provider}: {job_errors}" - ) - else: - for d in output["out_data"]: - datasets.append(d[1].dataset) - - return datasets - - def send(self, trans, history_id, bucket_name, authz_id, dataset_ids=None, overwrite_existing=False): - """ - Implements the logic of sending dataset(s) from a given history to a given cloud-based storage - (e.g., Amazon S3). - - :type trans: galaxy.webapps.base.webapp.GalaxyWebTransaction - :param trans: Galaxy web transaction - - :type history_id: string - :param history_id: the (encoded) id of history from which the object should be sent. - - :type bucket_name: string - :param bucket_name: the name of a bucket to which data should be sent (e.g., a bucket - name on AWS S3). - - :type authz_id: int - :param authz_id: the ID of CloudAuthz to be used for authorizing access to the resource provider. - You may get a list of the defined authorizations via `/api/cloud/authz`. Also, - you can use `/api/cloud/authz/create` to define a new authorization. - - :type dataset_ids: set - :param dataset_ids: [Optional] The list of (decoded) dataset ID(s) belonging to the given - history which should be sent to the given provider. If not provided, - Galaxy sends all the datasets belonging to the given history. - - :type overwrite_existing: boolean - :param overwrite_existing: [Optional] If set to "True", and an object with same name of the - dataset to be sent already exist in the bucket, Galaxy replaces - the existing object with the dataset to be sent. If set to - "False", Galaxy appends datetime to the dataset name to prevent - overwriting the existing object. - - :rtype: tuple - :return: A tuple of two lists of labels of the objects that were successfully and - unsuccessfully sent to cloud. - """ - if CloudProviderFactory is None: - raise Exception(NO_CLOUDBRIDGE_ERROR_MESSAGE) - - if not hasattr(trans.app, "authnz_manager"): - err_msg = ( - "The OpenID Connect protocol, a required feature for sending data to cloud, " - "is not enabled on this Galaxy instance." - ) - log.debug(err_msg) - raise MessageException(err_msg) - - cloudauthz = trans.app.authnz_manager.try_get_authz_config(trans.sa_session, trans.user.id, authz_id) - - history = trans.sa_session.get(model.History, history_id) - if not history: - raise ObjectNotFound("History with the provided ID not found.") - - sent = [] - failed = [] - for hda in history.datasets: - if hda.deleted or hda.purged or hda.state != "ok" or hda.creating_job.tool_id == SEND_TOOL: - continue - if dataset_ids is None or hda.dataset.id in dataset_ids: - try: - object_label = hda.name.replace(" ", "_") - args = { - # We encode ID here because the tool wrapper expects - # an encoded ID and attempts decoding it. - "authz_id": trans.security.encode_id(cloudauthz.id), - "bucket": bucket_name, - "object_label": object_label, - "filename": hda, - "overwrite_existing": overwrite_existing, - } - incoming = (util.Params(args, sanitize=False)).__dict__ - d2c = trans.app.toolbox.get_tool(SEND_TOOL, SEND_TOOL_VERSION) - if not d2c: - log.debug(f"Failed to get the `send` tool per user `{trans.user.id}` request.") - failed.append(json.dumps({"object": object_label, "error": "Unable to get the `send` tool."})) - continue - res = d2c.execute(trans, incoming, history=history) - job = res[0] - sent.append(json.dumps({"object": object_label, "job_id": trans.app.security.encode_id(job.id)})) - except Exception as e: - err_msg = f"maybe invalid or unauthorized credentials. {util.unicodify(e)}" - log.debug( - "Failed to send the dataset `%s` per user `%s` request to cloud, %s", - object_label, - trans.user.id, - err_msg, - ) - failed.append(json.dumps({"object": object_label, "error": err_msg})) - return sent, failed diff --git a/lib/galaxy/managers/cloudauthzs.py b/lib/galaxy/managers/cloudauthzs.py deleted file mode 100644 index bf4e0aa4caa2..000000000000 --- a/lib/galaxy/managers/cloudauthzs.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -Manager and (de)serializer for cloud authorizations (cloudauthzs). -""" - -import logging -from typing import Dict - -from galaxy import model -from galaxy.exceptions import InternalServerError -from galaxy.managers import ( - base, - sharable, -) - -log = logging.getLogger(__name__) - - -class CloudAuthzManager(sharable.SharableModelManager): - model_class = model.CloudAuthz - foreign_key_name = "cloudauthz" - - -class CloudAuthzsSerializer(base.ModelSerializer): - """ - Interface/service object for serializing cloud authorizations (cloudauthzs) into dictionaries. - """ - - model_manager_class = CloudAuthzManager - - def __init__(self, app, **kwargs): - super().__init__(app, **kwargs) - self.cloudauthzs_manager = self.manager - - self.default_view = "summary" - self.add_view( - "summary", - [ - "id", - "model_class", - "user_id", - "provider", - "config", - "authn_id", - "last_update", - "last_activity", - "create_time", - "description", - ], - ) - - def add_serializers(self): - super().add_serializers() - - # Arguments of the following lambda functions: - # i : an instance of galaxy.model.CloudAuthz. - # k : serialized dictionary key (e.g., 'model_class', 'provider'). - # **c: a dictionary containing 'trans' and 'user' objects. - serializers: Dict[str, base.Serializer] = { - "id": lambda item, key, **context: self.app.security.encode_id(item.id), - "model_class": lambda item, key, **context: "CloudAuthz", - "user_id": lambda item, key, **context: self.app.security.encode_id(item.user_id), - "provider": lambda item, key, **context: str(item.provider), - "config": lambda item, key, **context: item.config, - "authn_id": lambda item, key, **context: ( - self.app.security.encode_id(item.authn_id) if item.authn_id else None - ), - "last_update": lambda item, key, **context: str(item.last_update), - "last_activity": lambda item, key, **context: str(item.last_activity), - "create_time": lambda item, key, **context: item.create_time.isoformat(), - "description": lambda item, key, **context: str(item.description), - } - self.serializers.update(serializers) - - -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().add_deserializers() - self.deserializers.update( - { - "authn_id": self.deserialize_and_validate_authn_id, - "provider": self.default_deserializer, - "config": self.default_deserializer, - "description": 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. - """ - - decoded_authn_id = self.app.security.decode_id(val, object_name="authz") - - 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 diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index becb6d3df1c7..73f6296d2a18 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -793,7 +793,6 @@ class User(Base, Dictifiable, RepresentById): addresses: Mapped[List["UserAddress"]] = relationship( back_populates="user", order_by=lambda: desc(UserAddress.update_time), cascade_backrefs=False ) - cloudauthz: Mapped[List["CloudAuthz"]] = relationship(back_populates="user") custos_auth: Mapped[List["CustosAuthnzToken"]] = relationship(back_populates="user") default_permissions: Mapped[List["DefaultUserPermissions"]] = relationship(back_populates="user") groups: Mapped[List["UserGroupAssociation"]] = relationship(back_populates="user") @@ -10205,42 +10204,6 @@ class CustosAuthnzToken(Base, RepresentById): user: Mapped["User"] = relationship("User", back_populates="custos_auth") -class CloudAuthz(Base): - __tablename__ = "cloudauthz" - - id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("galaxy_user.id"), index=True) - provider: Mapped[Optional[str]] = mapped_column(String(255)) - config: Mapped[Optional[bytes]] = mapped_column(MutableJSONType) - authn_id: Mapped[Optional[int]] = mapped_column(ForeignKey("oidc_user_authnz_tokens.id"), index=True) - tokens: Mapped[Optional[bytes]] = mapped_column(MutableJSONType) - last_update: Mapped[Optional[datetime]] - last_activity: Mapped[Optional[datetime]] - description: Mapped[Optional[str]] = mapped_column(TEXT) - create_time: Mapped[datetime] = mapped_column(default=now, nullable=True) - user: Mapped[Optional["User"]] = relationship(back_populates="cloudauthz") - authn: Mapped[Optional["UserAuthnzToken"]] = relationship() - - def __init__(self, user_id, provider, config, authn_id, description=None): - self.user_id = user_id - self.provider = provider - self.config = config - self.authn_id = authn_id - self.last_update = now() - self.last_activity = now() - self.description = description - - def equals(self, user_id, provider, authn_id, config): - return ( - self.user_id == user_id - and self.provider == provider - and self.authn_id - and self.authn_id == authn_id - and len({k: self.config[k] for k in self.config if k in config and self.config[k] == config[k]}) - == len(self.config) - ) - - class Page(Base, HasTags, Dictifiable, RepresentById, UsesCreateAndUpdateTime): __tablename__ = "page" __table_args__ = (Index("ix_page_slug", "slug", mysql_length=200),) diff --git a/lib/galaxy/model/migrations/alembic/versions_gxy/570bce1e82f9_drop_cloudauthz_table.py b/lib/galaxy/model/migrations/alembic/versions_gxy/570bce1e82f9_drop_cloudauthz_table.py new file mode 100644 index 000000000000..1afe2dbe5058 --- /dev/null +++ b/lib/galaxy/model/migrations/alembic/versions_gxy/570bce1e82f9_drop_cloudauthz_table.py @@ -0,0 +1,47 @@ +"""drop cloudauthz table + +Revision ID: 570bce1e82f9 +Revises: c14a3c93d66b +Create Date: 2024-05-21 15:53:06.430082 + +""" + +import sqlalchemy as sa + +from galaxy.model.custom_types import MutableJSONType +from galaxy.model.migrations.util import ( + create_table, + drop_table, + transaction, +) +from galaxy.model.orm.now import now + +# revision identifiers, used by Alembic. +revision = "570bce1e82f9" +down_revision = "c14a3c93d66b" +branch_labels = None +depends_on = None + +CLOUDAUTHZ_TABLE_NAME = "cloudauthz" + + +def upgrade(): + with transaction(): + drop_table(CLOUDAUTHZ_TABLE_NAME) + + +def downgrade(): + with transaction(): + create_table( + CLOUDAUTHZ_TABLE_NAME, + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("user_id", sa.Integer, sa.ForeignKey("galaxy_user.id"), index=True), + sa.Column("provider", sa.String(255)), + sa.Column("config", MutableJSONType), + sa.Column("authn_id", sa.Integer, sa.ForeignKey("oidc_user_authnz_tokens.id"), index=True), + sa.Column("tokens", MutableJSONType), + sa.Column("last_update", sa.DateTime), + sa.Column("last_activity", sa.DateTime), + sa.Column("description", sa.Text), + sa.Column("create_time", sa.DateTime, default=now), + ) diff --git a/lib/galaxy/schema/cloud.py b/lib/galaxy/schema/cloud.py deleted file mode 100644 index 86a4f11a41ab..000000000000 --- a/lib/galaxy/schema/cloud.py +++ /dev/null @@ -1,131 +0,0 @@ -from typing import ( - List, - Optional, - Union, -) - -from pydantic import ( - Field, - RootModel, -) -from typing_extensions import Literal - -from galaxy.schema.fields import DecodedDatabaseIdField -from galaxy.schema.schema import ( - DatasetSummary, - Model, -) - - -class InputArguments(Model): - dbkey: Optional[str] = Field( - default="?", - title="Database Key", - description="Sets the database key of the objects being fetched to Galaxy.", - ) - file_type: Optional[str] = Field( - default="auto", - title="File Type", - description="Sets the Galaxy datatype (e.g., `bam`) for the objects being fetched to Galaxy. See the following link for a complete list of Galaxy data types: https://galaxyproject.org/learn/datatypes/.", - ) - to_posix_lines: Optional[Union[Literal["Yes"], bool]] = Field( - default="Yes", - title="POSIX line endings", - description="A boolean value ('true' or 'false'); if 'Yes', converts universal line endings to POSIX line endings. Set to 'False' if you upload a gzip, bz2 or zip archive containing a binary file.", - ) - space_to_tab: Optional[bool] = Field( - default=False, - title="Spaces to tabs", - description="A boolean value ('true' or 'false') that sets if spaces should be converted to tab in the objects being fetched to Galaxy. Applicable only if `to_posix_lines` is True", - ) - - -class CloudObjects(Model): - history_id: DecodedDatabaseIdField = Field( - default=..., - title="History ID", - description="The ID of history to which the object should be received to.", - ) - bucket: str = Field( - default=..., - title="Bucket", - description="The name of a bucket from which data should be fetched from (e.g., a bucket name on AWS S3).", - ) - objects: List[str] = Field( - default=..., - title="Objects", - description="A list of the names of objects to be fetched.", - ) - authz_id: DecodedDatabaseIdField = Field( - default=..., - title="Authentication ID", - description="The ID of CloudAuthz to be used for authorizing access to the resource provider. You may get a list of the defined authorizations via `/api/cloud/authz`. Also, you can use `/api/cloud/authz/create` to define a new authorization.", - ) - input_args: Optional[InputArguments] = Field( - default=None, - title="Input arguments", - description="A summary of the input arguments, which is optional and will default to {}.", - ) - - -class CloudDatasets(Model): - history_id: DecodedDatabaseIdField = Field( - default=..., - title="History ID", - description="The ID of history from which the object should be downloaded", - ) - bucket: str = Field( - default=..., - title="Bucket", - description="The name of a bucket to which data should be sent (e.g., a bucket name on AWS S3).", - ) - authz_id: DecodedDatabaseIdField = Field( - default=..., - title="Authentication ID", - description="The ID of CloudAuthz to be used for authorizing access to the resource provider. You may get a list of the defined authorizations via `/api/cloud/authz`. Also, you can use `/api/cloud/authz/create` to define a new authorization.", - ) - dataset_ids: Optional[List[DecodedDatabaseIdField]] = Field( - default=None, - title="Objects", - description="A list of dataset IDs belonging to the specified history that should be sent to the given bucket. If not provided, Galaxy sends all the datasets belonging the specified history.", - ) - overwrite_existing: Optional[bool] = Field( - default=False, - title="Spaces to tabs", - description="A boolean value. If set to 'True', and an object with same name of the dataset to be sent already exist in the bucket, Galaxy replaces the existing object with the dataset to be sent. If set to 'False', Galaxy appends datetime to the dataset name to prevent overwriting an existing object.", - ) - - -class CloudDatasetsResponse(Model): - sent_dataset_labels: List[str] = Field( - default=..., - title="Send datasets", - description="The datasets for which Galaxy succeeded to create (and queue) send job", - ) - failed_dataset_labels: List[str] = Field( - default=..., - title="Failed datasets", - description="The datasets for which Galaxy failed to create (and queue) send job", - ) - bucket_name: str = Field( - default=..., - title="Bucket", - description="The name of bucket to which the listed datasets are queued to be sent", - ) - - -class StatusCode(Model): - detail: str = Field( - default=..., - title="Detail", - description="The detail to expand on the status code", - ) - status: int = Field( - default=..., - title="Code", - description="The actual status code", - ) - - -class DatasetSummaryList(RootModel): - root: List[DatasetSummary] diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index ea3959955bec..66a331ad0ef8 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -3728,16 +3728,3 @@ class PageSummaryList(RootModel): default=[], title="List with summary information of Pages.", ) - - -class DatasetSummary(Model): - id: EncodedDatabaseIdField - create_time: Optional[datetime] = CreateTimeField - update_time: Optional[datetime] = UpdateTimeField - state: DatasetStateField - deleted: bool - purged: bool - purgable: bool - file_size: int - total_size: int - uuid: UuidField diff --git a/lib/galaxy/webapps/galaxy/api/cloud.py b/lib/galaxy/webapps/galaxy/api/cloud.py deleted file mode 100644 index 059b6732e04c..000000000000 --- a/lib/galaxy/webapps/galaxy/api/cloud.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -API operations on Cloud-based storages, such as Amazon Simple Storage Service (S3). -""" - -import logging - -from fastapi import Body - -from galaxy.managers.cloud import CloudManager -from galaxy.managers.context import ProvidesHistoryContext -from galaxy.managers.datasets import DatasetSerializer -from galaxy.schema.cloud import ( - CloudDatasets, - CloudDatasetsResponse, - CloudObjects, - DatasetSummaryList, -) -from galaxy.webapps.galaxy.api import ( - depends, - Router, -) -from . import DependsOnTrans - -log = logging.getLogger(__name__) - -router = Router(tags=["cloud"]) - - -@router.cbv -class FastAPICloudController: - cloud_manager: CloudManager = depends(CloudManager) - datasets_serializer: DatasetSerializer = depends(DatasetSerializer) - - @router.post( - "/api/cloud/storage/get", - summary="Gets given objects from a given cloud-based bucket to a Galaxy history.", - deprecated=True, - ) - def get( - self, - payload: CloudObjects = Body(default=...), - trans: ProvidesHistoryContext = DependsOnTrans, - ) -> DatasetSummaryList: - datasets = self.cloud_manager.get( - trans=trans, - history_id=payload.history_id, - bucket_name=payload.bucket, - objects=payload.objects, - authz_id=payload.authz_id, - input_args=payload.input_args, - ) - rtv = [] - for dataset in datasets: - rtv.append(self.datasets_serializer.serialize_to_view(dataset, view="summary")) - return DatasetSummaryList.model_construct(root=rtv) - - @router.post( - "/api/cloud/storage/send", - summary="Sends given dataset(s) in a given history to a given cloud-based bucket.", - deprecated=True, - ) - def send( - self, - payload: CloudDatasets = Body(default=...), - trans: ProvidesHistoryContext = DependsOnTrans, - ) -> CloudDatasetsResponse: - log.info( - msg="Received api/send request for `{}` datasets using authnz with id `{}`, and history `{}`." - "".format( - "all the dataset in the given history" if not payload.dataset_ids else len(payload.dataset_ids), - payload.authz_id, - payload.history_id, - ) - ) - - sent, failed = self.cloud_manager.send( - trans=trans, - history_id=payload.history_id, - bucket_name=payload.bucket, - authz_id=payload.authz_id, - dataset_ids=payload.dataset_ids, - overwrite_existing=payload.overwrite_existing, - ) - return CloudDatasetsResponse(sent_dataset_labels=sent, failed_dataset_labels=failed, bucket_name=payload.bucket) diff --git a/lib/galaxy/webapps/galaxy/api/cloudauthz.py b/lib/galaxy/webapps/galaxy/api/cloudauthz.py deleted file mode 100644 index e3e1ced53061..000000000000 --- a/lib/galaxy/webapps/galaxy/api/cloudauthz.py +++ /dev/null @@ -1,277 +0,0 @@ -""" -API operations on defining cloud authorizations. - -Through means of cloud authorization a user is able to grant a Galaxy server a secure access to his/her -cloud-based resources without sharing his/her long-lasting credentials. - -User provides a provider-specific configuration, which Galaxy users to request temporary credentials -from the provider to access the user's resources. -""" - -import logging - -from galaxy.exceptions import ( - ActionInputError, - InternalServerError, - MalformedId, - RequestParameterInvalidException, - RequestParameterMissingException, -) -from galaxy.managers import cloudauthzs -from galaxy.model.base import transaction -from galaxy.structured_app import StructuredApp -from galaxy.util import unicodify -from galaxy.web import expose_api -from . import BaseGalaxyAPIController - -log = logging.getLogger(__name__) - - -class CloudAuthzController(BaseGalaxyAPIController): - """ - RESTfull controller for defining cloud authorizations. - """ - - def __init__(self, app: StructuredApp): - super().__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): - """ - GET /api/cloud/authz - - Lists all the cloud authorizations user has defined. - - :type trans: galaxy.webapps.base.webapp.GalaxyWebTransaction - :param trans: Galaxy web transaction - - :param kwargs: empty dict - - :rtype: list of dict - :return: a list of cloud authorizations (each represented in key-value pair format) defined for the user. - """ - rtv = [] - for cloudauthz in trans.user.cloudauthz: - rtv.append( - self.cloudauthz_serializer.serialize_to_view( - cloudauthz, user=trans.user, trans=trans, **self._parse_serialization_params(kwargs, "summary") - ) - ) - return rtv - - @expose_api - def create(self, trans, payload, **kwargs): - """ - * POST /api/cloud/authz - Request to store the payload as a cloudauthz (cloud authorization) configuration for a user. - - :type trans: galaxy.webapps.base.webapp.GalaxyWebTransaction - :param trans: Galaxy web transaction - - :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. See the following page for details: - https://galaxyproject.org/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/authnz/config/ - - * description: [Optional] a brief description for this configuration. - - :param kwargs: empty dict - - :rtype: dict - :return: a dictionary with the following kvp: - * status: HTTP response code - * message: A message complementary to the response code. - """ - msg_template = f"Rejected user `{trans.user.id}`'s request to create cloudauthz config because of {{}}." - if not isinstance(payload, dict): - raise ActionInputError( - "Invalid payload data type. The payload is expected to be a dictionary, but " - f"received data of type `{type(payload)}`." - ) - - missing_arguments = [] - provider = payload.get("provider", None) - if provider is None: - missing_arguments.append("provider") - - config = payload.get("config", None) - if config is None: - missing_arguments.append("config") - - authn_id = payload.get("authn_id", None) - if authn_id is None and provider.lower() not in ["azure", "gcp"]: - missing_arguments.append("authn_id") - - if len(missing_arguments) > 0: - log.debug(msg_template.format(f"missing required config {missing_arguments}")) - raise RequestParameterMissingException( - f"The following required arguments are missing in the payload: {missing_arguments}" - ) - - description = payload.get("description", "") - - if not isinstance(config, dict): - log.debug(msg_template.format(f"invalid config type `{type(config)}`, expected `dict`")) - raise RequestParameterInvalidException( - f"Invalid type for the required `config` variable; expected `dict` but received `{type(config)}`." - ) - if authn_id: - try: - decoded_authn_id = self.decode_id(authn_id) - except MalformedId as e: - log.debug(msg_template.format(f"cannot decode authz_id `{authn_id}`")) - raise e - - try: - trans.app.authnz_manager.can_user_assume_authn(trans, decoded_authn_id) - except Exception as e: - raise e - - # No two authorization configuration with - # exact same key/value should exist. - for ca in trans.user.cloudauthz: - if ca.equals(trans.user.id, provider, authn_id, config): - log.debug( - "Rejected user `%s`'s request to create cloud authorization because a similar config " - "already exists.", - trans.user.id, - ) - raise ActionInputError("A similar cloud authorization configuration is already defined.") - - try: - new_cloudauthz = self.cloudauthz_manager.create( - user_id=trans.user.id, provider=provider, config=config, authn_id=authn_id, description=description - ) - view = self.cloudauthz_serializer.serialize_to_view( - new_cloudauthz, trans=trans, **self._parse_serialization_params(kwargs, "summary") - ) - log.debug(f"Created a new cloudauthz record for the user id `{str(trans.user.id)}` ") - return view - except Exception as e: - log.exception(msg_template.format("exception while creating the new cloudauthz record")) - raise InternalServerError( - "An unexpected error has occurred while responding to the create request of the " - "cloudauthz API." + unicodify(e) - ) - - @expose_api - def delete(self, trans, encoded_authz_id, **kwargs): - """ - * DELETE /api/cloud/authz/{encoded_authz_id} - Deletes the CloudAuthz record with the given ``encoded_authz_id`` from database. - - :type trans: galaxy.webapps.base.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 marked deleted. - - :rtype JSON - :return The cloudauthz record marked as deleted, serialized as a JSON object. - """ - - msg_template = f"Rejected user `{str(trans.user.id)}`'s request to delete cloudauthz config because of {{}}." - try: - authz_id = self.decode_id(encoded_authz_id) - except MalformedId as e: - log.debug(msg_template.format(f"cannot decode authz_id `{encoded_authz_id}`")) - raise e - - try: - cloudauthz = trans.app.authnz_manager.try_get_authz_config(trans.sa_session, trans.user.id, authz_id) - trans.sa_session.delete(cloudauthz) - with transaction(trans.sa_session): - trans.sa_session.commit() - log.debug(f"Deleted a cloudauthz record with id `{authz_id}` for the user id `{str(trans.user.id)}` ") - view = self.cloudauthz_serializer.serialize_to_view( - cloudauthz, trans=trans, **self._parse_serialization_params(kwargs, "summary") - ) - trans.response.status = "200" - return view - except Exception as e: - log.exception( - msg_template.format(f"exception while deleting the cloudauthz record with ID: `{encoded_authz_id}`.") - ) - raise InternalServerError( - "An unexpected error has occurred while responding to the DELETE request of the " - "cloudauthz API." + unicodify(e) - ) - - @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.webapps.base.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/authnz/config/ - - 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/authnz/ - - * deleted: a boolean type marking the specified cloudauthz as (un)deleted. - - """ - - msg_template = f"Rejected user `{str(trans.user.id)}`'s request to delete cloudauthz config because of {{}}." - try: - authz_id = self.decode_id(encoded_authz_id) - except MalformedId as e: - log.debug(msg_template.format(f"cannot decode authz_id `{encoded_authz_id}`")) - raise e - - 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(f"exception while updating the cloudauthz record with ID: `{encoded_authz_id}`.") - ) - raise InternalServerError( - "An unexpected error has occurred while responding to the PUT request of the " - "cloudauthz API." + unicodify(e) - ) diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index a5d7c5e04e99..8f428563426d 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -365,27 +365,6 @@ def populate_api_routes(webapp, app): parent_resources=dict(member_name="page", collection_name="pages"), ) - webapp.mapper.connect("/api/cloud/authz/", action="index", controller="cloudauthz", conditions=dict(method=["GET"])) - webapp.mapper.connect( - "/api/cloud/authz/", action="create", controller="cloudauthz", conditions=dict(method=["POST"]) - ) - - webapp.mapper.connect( - "delete_cloudauthz_item", - "/api/cloud/authz/{encoded_authz_id}", - action="delete", - 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"]), - ) - # ======================= # ====== TOOLS API ====== # ======================= diff --git a/test/integration/cloudauthz/__init__.py b/test/integration/cloudauthz/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/test/integration/cloudauthz/test_cloudauthz.py b/test/integration/cloudauthz/test_cloudauthz.py deleted file mode 100644 index 6444c303d830..000000000000 --- a/test/integration/cloudauthz/test_cloudauthz.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -You may run this test using the following command: -./run_tests.sh test/integration/cloudauthz/test_cloudauthz.py:TestDefineCloudAuthz.test_post_cloudauthz_without_authn -s -""" - -import json - -from galaxy_test.driver import integration_util - - -class TestDefineCloudAuthz(integration_util.IntegrationTestCase): - framework_tool_and_types = True - - def test_post_cloudauthz_without_authn(self): - """ - This test asserts if a cloudauthz object - can be successfully posted to the cloudauthz API - (i.e., api/cloud/authz). - """ - provider = "azure" - tenant_id = "abc" - client_id = "def" - client_secret = "ghi" - with self._different_user("vahid@test.com"): - # The payload for the POST API. - payload = { - "provider": provider, - "config": json.dumps({"tenant_id": tenant_id, "client_id": client_id, "client_secret": client_secret}), - } - - response = self._post(path="cloud/authz", data=payload) - response.raise_for_status() - cloudauthz = response.json() - - assert cloudauthz["provider"] == provider diff --git a/tools/data_export/send.py b/tools/data_export/send.py deleted file mode 100644 index 87270b1d8fad..000000000000 --- a/tools/data_export/send.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -This tool implements the logic of sending data -from Galaxy to a cloud-based storage. - -This tool depends on the CloudManager for configuring -a connection to a cloud-based resource provider. Also, -it leverages Cloudbridge (github.com/CloudVE/cloudbridge) -to send a dataset to a cloud-based storage. - -""" - -import argparse -import datetime -import json -import os -import sys -import time - -from galaxy.exceptions import ObjectNotFound -from galaxy.managers.cloud import CloudManager - -try: - from cloudbridge.factory import ( - CloudProviderFactory, - ProviderList, - ) -except ImportError: - CloudProviderFactory = None - ProviderList = None - -NO_CLOUDBRIDGE_ERROR_MESSAGE = ( - "Cloud ObjectStore is configured, but no CloudBridge dependency available." - "Please install CloudBridge or modify ObjectStore configuration." -) - - -def load_credential(credentials_file): - print("[1/5 {}] Reading cloud authorization.".format(datetime.datetime.now().replace(microsecond=0))) - with open(credentials_file) as f: - credentials = f.read() - os.remove(credentials_file) - return json.loads(credentials) - - -def send(provider, credentials, bucket, object_label, filename, overwrite_existing): - start_time = time.time() - if not os.path.exists(filename): - raise Exception("The file `{}` does not exist.".format(filename)) - if CloudProviderFactory is None: - raise Exception(NO_CLOUDBRIDGE_ERROR_MESSAGE) - - print("[2/5 {}] Establishing a connection to {}.".format(datetime.datetime.now().replace(microsecond=0), provider)) - try: - connection = CloudManager.configure_provider(provider, credentials) - except Exception: - print("Failed to establish the connection.") - - print("[3/5 {}] Accessing bucket {}.".format(datetime.datetime.now().replace(microsecond=0), bucket)) - bucket_obj = connection.storage.buckets.get(bucket) - if bucket_obj is None: - raise ObjectNotFound("Could not find the specified bucket `{}`.".format(bucket)) - if overwrite_existing is False and bucket_obj.objects.get(object_label) is not None: - object_label += "_" + datetime.datetime.now().strftime("%y%m%d_%H%M%S") - - print("[4/5 {}] Creating object {}.".format(datetime.datetime.now().replace(microsecond=0), object_label)) - try: - created_obj = bucket_obj.objects.create(object_label) - except Exception: - print("Failed to create the object.") - - print("[5/5 {}] Sending dataset.".format(datetime.datetime.now().replace(microsecond=0))) - transfer_start_time = time.time() - try: - created_obj.upload_from_file(filename) - except Exception: - print("Failed to send the dataset.") - - print("Finished successfully.") - print("Job runtime:\t{}".format(time.time() - start_time)) - print( - "Transfer ET:\t{}\tSpeed:\t{}MB/sec".format( - time.time() - transfer_start_time, - round((os.path.getsize(filename) >> 20) / (time.time() - transfer_start_time), 3), - ) - ) - - -def parse_args(args): - parser = argparse.ArgumentParser() - parser.add_argument("-p", "--provider", type=str, required=True, help="Provider") - - parser.add_argument( - "-b", - "--bucket", - type=str, - required=True, - help="The cloud-based storage bucket in which data should be written.", - ) - - parser.add_argument( - "-o", - "--object_label", - type=str, - required=True, - help="The label of the object created on the cloud-based storage for the data to be persisted.", - ) - - parser.add_argument( - "-f", - "--filename", - type=str, - required=True, - help="The (absolute) filename of the data to be persisted on the cloud-based storage.", - ) - - parser.add_argument( - "-w", - "--overwrite_existing", - type=str, - required=True, - help="Sets if an object with the given `object_label` exists, this tool " - "should overwrite it (true) or append a time stamp to avoid " - "overwriting (false).", - ) - - parser.add_argument("--credentials_file", type=str, required=True, help="Credentials file") - - return parser.parse_args(args) - - -def __main__(): - args = parse_args(sys.argv[1:]) - overwrite_existing = args.overwrite_existing.lower() == "true" - credentials = load_credential(args.credentials_file) - send(args.provider, credentials, args.bucket, args.object_label, args.filename, overwrite_existing) - - -if __name__ == "__main__": - sys.exit(__main__()) diff --git a/tools/data_export/send.xml b/tools/data_export/send.xml deleted file mode 100644 index 62e5ccffd943..000000000000 --- a/tools/data_export/send.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - &2 - #else - python $__tool_directory__/send.py - --provider=$authz_record.provider - --filename=$filename - --credentials_file=$credentials_file - --bucket=$bucket - --object_label=$filename.name.replace(" ", "_") - --overwrite_existing=$overwrite_existing - #end try - ]]> - - - - - - - - - - -**What it does** - Send dataset(s) from Galaxy to cloud-based storage (e.g., AWS S3). - - Supply a name of the target bucket where the data will be sent along - with an id for the desired cloud authorization. To get the list of Cloud - Authorization IDs available for your account, visit `/api/cloud/authz`. - -**Remarks** - This tool leverages OpenID Connect protocol and CloudAuthz (https://github.com/galaxyproject/cloudauthz) - to access cloud-based resources without requiring user credentials. - - If selecting multiple datasets, make sure each dataset is named - differently; otherwise, only one copy of the selected datasets will be - copied. - -