Drop CloudAuth client files

This commit is contained in:
John Davis
2024-05-21 14:18:12 -04:00
parent 1760625786
commit 0d533da34b
18 changed files with 0 additions and 1323 deletions
@@ -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();
});
});
});
@@ -1,372 +0,0 @@
<template>
<section class="cloud-auth">
<header>
<b-alert dismissible fade variant="warning" :show="errorMessage !== null" @dismissed="errorMessage = null">
{{ errorMessage }}
</b-alert>
<hgroup class="cloud-auth-title">
<h1 v-localize class="h-lg">Manage Cloud Authorization</h1>
<nav class="operations">
<ul>
<li class="cloudKeyHelp">
<a
v-b-tooltip.hover
:class="{ active: showHelp }"
aria-label="Instructions"
title="Instructions"
@click.prevent="showHelp = !showHelp">
<span>Instructions</span>
</a>
</li>
<li class="cloudKeyFilter">
<a
v-b-tooltip.hover
:class="{ active: showFilter }"
aria-label="Filter Results"
title="Filter Results"
@click.prevent="showFilter = !showFilter">
<span>Filter</span>
</a>
</li>
<li class="createCloudKey">
<a
v-b-tooltip.hover
aria-label="Create New Key"
title="Create New Key"
@click.prevent="onCreate">
<span>Create New Key</span>
</a>
</li>
</ul>
</nav>
</hgroup>
<transition name="fade">
<hgroup v-if="showHelp">
<p>See the online <a href="https://galaxyproject.org/authnz/">Documentation</a>.</p>
</hgroup>
</transition>
<transition name="fade">
<hgroup v-if="showFilter">
<b-form-group :description="filterDescription">
<b-form-input v-model="filter" type="text" placeholder="Filter" />
</b-form-group>
</hgroup>
</transition>
<b-button-group class="mb-4">
<button name="createNewKey" @click="onCreate">Create New Authorization Key</button>
</b-button-group>
</header>
<div class="scroll-container">
<b-list-group>
<transition-group name="fade">
<CloudAuthItem
v-for="credential in filteredItems"
:key="credential.counter"
:credential="credential"
class="mb-1"
@delete="onDelete"
@save="onSave"
@expand="expand(credential, $event)" />
</transition-group>
</b-list-group>
</div>
<b-modal
id="deleteCredentialModal"
ref="deleteModal"
v-model="hasDoomed"
centered
title="Delete Key?"
size="sm"
@ok="deleteKey"
@cancel="doomedItem = null">
</b-modal>
</section>
</template>
<script>
import BootstrapVue from "bootstrap-vue";
import Vue from "vue";
import CloudAuthItem from "./CloudAuthItem";
import { Credential } from "./model";
import svc from "./model/service";
Vue.use(BootstrapVue);
export default {
components: {
CloudAuthItem,
},
data() {
return {
items: [],
filter: "",
showHelp: true,
showFilter: false,
loading: false,
doomedItem: null,
errorMessage: null,
};
},
computed: {
filteredItems() {
return this.items.filter((o) => o.match(this.filter));
},
filterDescription() {
return `${this.filteredItems.length} matches out of ${this.items.length} items`;
},
deleteButtonVariant() {
return this.showDeleted ? "primary" : "secondary";
},
hasDoomed: {
get() {
return this.doomedItem !== null;
},
// This setter is here because vue-bootstrap modal
// tries to set this property for unfathomable reasons
set() {},
},
},
watch: {
showDeleted(deleted) {
this.loadCredentials({ deleted });
},
},
created() {
this.loadCredentials();
},
methods: {
loadCredentials(params = {}) {
this.loading = true;
svc.listCredentials(params)
.then((items) => (this.items = items))
.catch(this.setError("Unable to load cloud keys."))
.finally(() => (this.loading = false));
},
onCreate() {
const newItem = Credential.create({ expanded: true });
this.addItem(newItem);
},
onSave(item) {
if (!item.valid) {
return;
}
item.loading = true;
svc.saveCredential(item)
.then((result) => {
item.id = result.id;
item.updateState();
})
.catch(this.setError("Unable to save key."))
.finally(() => (item.loading = false));
},
onDelete(doomed) {
this.doomedItem = doomed;
if (doomed.id) {
this.$refs.deleteModal.show();
} else {
this.removeItem(doomed);
this.doomedItem = null;
}
},
deleteKey() {
// Called when the modal is closed with an "OK"
svc.deleteCredential(this.doomedItem)
.then(() => this.removeItem(this.doomedItem))
.catch(this.setError("Unable to delete cloud key."))
.finally(() => {
this.doomedItem = null;
});
},
expand(credential, { expanded }) {
credential.expanded = expanded;
if (expanded) {
this.items.filter((i) => i !== credential).forEach((i) => (i.expanded = false));
}
},
addItem(item) {
this.items = [...this.items, item];
},
removeItem(item) {
this.items = this.items.filter((o) => o !== item);
},
setError(msg) {
return (err) => {
this.errorMessage = msg;
console.warn(err);
};
},
},
};
</script>
<style lang="scss">
@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";
// TODO: build reusable icon menu? Maybe make into a component?
// The existing one has lots of reusability problems
.operations {
margin-bottom: 0;
ul {
@include list_reset();
display: flex;
li:not(:first-child) {
@extend .ml-2;
}
}
}
// 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;
// top icon menu
.operations {
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);
}
}
}
}
// Single list item
.cloud-auth-key {
header hgroup {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
> * {
margin-bottom: 0;
}
}
form {
@extend .my-3;
@extend .pt-3;
// removes wierd double arrows on select
.custom-select {
background: none;
}
// Undo butchering from base.css so side-by-side labels work
.form-row {
display: flex;
input,
select {
max-width: none;
}
label {
font-weight: 400;
}
}
// button list at bottom of form
footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
@extend .pt-3;
button:not(:first-child) {
@extend .ml-1;
}
}
}
// 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);
}
}
}
// Transitions
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
// Delete modal
#deleteCredentialModal {
.modal-body {
display: none;
}
.modal-dialog {
max-width: 300px;
}
}
</style>
@@ -1,93 +0,0 @@
<template>
<b-list-group-item class="cloud-auth-key" :class="statusClasses" :variant="variant" :button="!expanded">
<header>
<hgroup>
<h2 class="h-sm" @click.prevent="expand()">{{ credential.title }}</h2>
<nav class="operations">
<ul>
<transition name="fade">
<li v-if="expanded && credential.dirty" class="save">
<a
v-b-tooltip.hover
aria-label="Save Key"
title="Save Key"
@click.prevent="$emit('save', credential)">
<span>Save Key</span>
</a>
</li>
</transition>
<li class="delete">
<a
v-b-tooltip.hover
aria-label="Delete Key"
title="Delete Key"
@click.prevent="$emit('delete', credential)">
<span>Delete Key</span>
</a>
</li>
<li class="details">
<a
v-b-tooltip.hover
aria-label="Show Details"
title="Show Details"
@click.prevent="expand()">
<span>Details</span>
</a>
</li>
</ul>
</nav>
</hgroup>
</header>
<!-- TODO: Restructure credential handling so we're not mutating the prop -->
<!-- eslint-disable vue/no-mutating-props-->
<CredentialForm
v-if="expanded"
v-model="credential"
class="border-top"
@click.self="expand()"
@save="$emit('save', credential)"
@delete="$emit('delete', credential)" />
<!-- eslint-enable vue/no-mutating-props-->
</b-list-group-item>
</template>
<script>
import CredentialForm from "./CredentialForm";
import { Credential } from "./model";
export default {
components: {
CredentialForm,
},
props: {
credential: { type: Credential, required: true },
},
computed: {
statusClasses() {
const { expanded, valid, dirty, loading } = this.credential;
return { loading, expanded, valid, dirty, collapsed: !expanded };
},
variant() {
if (this.expanded) {
return "";
}
if (this.credential.dirty) {
return "warning";
}
if (!this.credential.valid) {
return "danger";
}
return "primary";
},
expanded() {
return this.credential.expanded;
},
},
methods: {
expand(forceState) {
const expanded = forceState !== undefined ? forceState : !this.expanded;
this.$emit("expand", { expanded });
},
},
};
</script>
@@ -1,42 +0,0 @@
<template>
<div>
<b-form-group
v-for="(field, fieldName) in fieldDefs"
:key="fieldName"
:label="field.label || fieldName"
:label-for="'config-' + fieldName"
label-cols-lg="3"
:state="config.fieldValid(fieldName)"
:invalid-feedback="config.errorMessage(fieldName)">
<MaskedInput
:id="'config-' + fieldName"
v-model="config[fieldName]"
:mask="field.mask"
:placeholder="field.placeholder"
:maxlength="field.maxlength || Math.Infinity"
:state="config.fieldValid(fieldName)"
trim />
</b-form-group>
</div>
</template>
<script>
import MaskedInput from "components/MaskedInput";
export default {
components: {
MaskedInput,
},
props: {
value: { type: Object, required: true },
},
computed: {
config() {
return this.value;
},
fieldDefs() {
return this.value.constructor.fields;
},
},
};
</script>
@@ -1,101 +0,0 @@
<template>
<b-form :validated="credential.isValid">
<!-- user label -->
<b-form-group label="Description" label-for="credentialDescription" label-cols-lg="3">
<b-form-input
id="credentialDescription"
v-model="credential.description"
:state="credential.fieldValid('description')"
placeholder="Description (optional)"
trim />
</b-form-group>
<!-- google-openidconnect -->
<b-form-group
v-if="identityProviders.length > 1"
label="Identity Provider"
label-for="identityProvider"
label-cols-lg="3">
<b-form-select
id="identityProvider"
v-model="credential.authn_id"
:state="credential.fieldValid('authn_id')"
:options="identityProviders" />
</b-form-group>
<!-- aws/azure, etc -->
<b-form-group label="Resource Provider" label-for="resourceProvider" label-cols-lg="3">
<b-form-select
id="resourceProvider"
v-model="credential.resourceProvider"
:state="credential.fieldValid('provider')"
:options="resourceProviderOptions" />
</b-form-group>
<CredentialConfig v-model="credential.config" />
<footer class="border-top">
<b-button variant="secondary" @click.prevent="$emit('delete', credential)"> Delete Key </b-button>
<b-button
aria-label="Save Key"
:variant="saveButtonVariant"
:disabled="saveButtonDisabled"
@click.prevent="$emit('save', credential)">
{{ saveButtonTitle }}
</b-button>
</footer>
</b-form>
</template>
<script>
import CredentialConfig from "./CredentialConfig";
import { Credential, ResourceProviders } from "./model";
import { getIdentityProviders } from "./model/service";
export default {
components: {
CredentialConfig,
},
props: {
value: { type: Credential, required: true },
},
data() {
return {
identityProviders: [],
};
},
computed: {
credential() {
return this.value;
},
// transformed for bootstrap component
resourceProviderOptions() {
return Array.from(ResourceProviders.entries()).map(([value, o]) => ({ value, text: o.label }));
},
config() {
return this.credential.config;
},
loading() {
return this.credential.loading;
},
saveButtonDisabled() {
return !(this.credential.valid && this.credential.dirty);
},
saveButtonVariant() {
return this.saveButtonDisabled ? "secondary" : "primary";
},
saveButtonTitle() {
return this.loading ? "Saving Key..." : "Save Key";
},
},
created() {
getIdentityProviders().then((result) => {
if (!this.credential.authn_id && result.length == 1) {
this.credential.authn_id = result[0].authn_id;
this.credential.updateState();
}
this.identityProviders = result;
});
},
};
</script>
@@ -1 +0,0 @@
export { default as CloudAuth } from "./CloudAuth.vue";
@@ -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",
},
};
@@ -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();
});
});
@@ -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.",
},
};
@@ -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();
});
});
});
@@ -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);
}
}
@@ -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;
});
@@ -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();
});
});
});
@@ -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);
}
}
@@ -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",
});
@@ -1,3 +0,0 @@
export { Credential } from "./Credential";
export { IdentityProvider } from "./IdentityProvider";
export { ResourceProviders } from "./ResourceProviders";
@@ -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,
};
@@ -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"
}
]