feat: Make security tokens optional (#1022)
Security tokens are a great way to protect sensitive data from being stored in plain text within your project file, but at the same time can be more difficult to use when sharing projects with team members. In this feature security tokens are still enabled by default but give the project author the ability to turn off security tokens which then stores all provider options in plain text.
Esse commit está contido em:
@@ -5,6 +5,9 @@ pr:
|
|||||||
- dev* # kick off for pr targeting dev or prefix dev
|
- dev* # kick off for pr targeting dev or prefix dev
|
||||||
- master # trigger build for pr targeting master
|
- master # trigger build for pr targeting master
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- group: CODE_COV
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Linux
|
- job: Linux
|
||||||
pool:
|
pool:
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ const common = require('./webpack.common.js')
|
|||||||
module.exports = merge(common, {
|
module.exports = merge(common, {
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
devtool: "inline-source-map",
|
devtool: "inline-source-map",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ export const english: IAppStrings = {
|
|||||||
title: "Security Token",
|
title: "Security Token",
|
||||||
description: "Used to encrypt sensitive data within project files",
|
description: "Used to encrypt sensitive data within project files",
|
||||||
},
|
},
|
||||||
|
useSecurityToken: {
|
||||||
|
title: "Use Security Token",
|
||||||
|
description: "When enabled will encrypt sensitive data within provider configuration",
|
||||||
|
},
|
||||||
save: "Save Project",
|
save: "Save Project",
|
||||||
sourceConnection: {
|
sourceConnection: {
|
||||||
title: "Source Connection",
|
title: "Source Connection",
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ export const spanish: IAppStrings = {
|
|||||||
title: "Token de seguridad",
|
title: "Token de seguridad",
|
||||||
description: "Se utiliza para cifrar datos confidenciales dentro de archivos de proyecto",
|
description: "Se utiliza para cifrar datos confidenciales dentro de archivos de proyecto",
|
||||||
},
|
},
|
||||||
|
useSecurityToken: {
|
||||||
|
title: "Usar Token de Seguridad",
|
||||||
|
description: "Si está habilitado, los datos confidenciales se cifrarán",
|
||||||
|
},
|
||||||
save: "Guardar el Proyecto",
|
save: "Guardar el Proyecto",
|
||||||
sourceConnection: {
|
sourceConnection: {
|
||||||
title: "Conexión de Origen",
|
title: "Conexión de Origen",
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ export const japanese: IAppStrings = {
|
|||||||
title: "セキュリティ トークン", // Security Token,
|
title: "セキュリティ トークン", // Security Token,
|
||||||
description: "プロジェクト ファイル内の機密データを暗号化するために使用されます", // Used to encrypt sensitive data within project file"
|
description: "プロジェクト ファイル内の機密データを暗号化するために使用されます", // Used to encrypt sensitive data within project file"
|
||||||
},
|
},
|
||||||
|
useSecurityToken: {
|
||||||
|
title: "セキュリティ トークン", // Use Security Token
|
||||||
|
description: "有効にすると、プロバイダー構成内の機密データが暗号化されます。",
|
||||||
|
// When enabled will encrypt sensitive data within provider configuration
|
||||||
|
},
|
||||||
save: "プロジェクトを保存", // Save Project,
|
save: "プロジェクトを保存", // Save Project,
|
||||||
sourceConnection: {
|
sourceConnection: {
|
||||||
title: "ソース接続", // Source Connection,
|
title: "ソース接続", // Source Connection,
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ export const korean: IAppStrings = {
|
|||||||
title: "보안 토큰", // Security Token,
|
title: "보안 토큰", // Security Token,
|
||||||
description: "프로젝트 파일 내에서 중요한 데이터를 암호화하는 데 사용", // Used to encrypt sensitive data within project file
|
description: "프로젝트 파일 내에서 중요한 데이터를 암호화하는 데 사용", // Used to encrypt sensitive data within project file
|
||||||
},
|
},
|
||||||
|
useSecurityToken: {
|
||||||
|
title: "보안 토큰 사용", // Use Security Token
|
||||||
|
description: "활성화되면 공급자 구성 내에서 중요한 데이터를 암호화합니다.",
|
||||||
|
// When enabled will encrypt sensitive data within provider configuration
|
||||||
|
},
|
||||||
save: "프로젝트 저장", // Save Project,
|
save: "프로젝트 저장", // Save Project,
|
||||||
sourceConnection: {
|
sourceConnection: {
|
||||||
title: "소스 연결", // Source Connection,
|
title: "소스 연결", // Source Connection,
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ export const chinese: IAppStrings = {
|
|||||||
title: "安全令牌", // Security Token
|
title: "安全令牌", // Security Token
|
||||||
description: "用于加密项目文件中的敏感数据", // Used to encrypt sensitive data within project files
|
description: "用于加密项目文件中的敏感数据", // Used to encrypt sensitive data within project files
|
||||||
},
|
},
|
||||||
|
useSecurityToken: {
|
||||||
|
title: "使用安全令牌", // Use Security Token
|
||||||
|
description: "启用后将在提供者配置内加密敏感数据",
|
||||||
|
// When enabled will encrypt sensitive data within provider configuration
|
||||||
|
},
|
||||||
save: "保存项目", // Save Project
|
save: "保存项目", // Save Project
|
||||||
sourceConnection: {
|
sourceConnection: {
|
||||||
title: "源连接", // Source Connection
|
title: "源连接", // Source Connection
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ export const chinesetw: IAppStrings = {
|
|||||||
title: "安全性權杖", // Security Token
|
title: "安全性權杖", // Security Token
|
||||||
description: "用於加密專案檔案中的敏感資料", // Used to encrypt sensitive data within project files
|
description: "用於加密專案檔案中的敏感資料", // Used to encrypt sensitive data within project files
|
||||||
},
|
},
|
||||||
|
useSecurityToken: {
|
||||||
|
title: "使用安全令牌", // Use Security Token
|
||||||
|
description: "啟用後將在提供者配置內加密敏感數據",
|
||||||
|
// When enabled will encrypt sensitive data within provider configuration
|
||||||
|
},
|
||||||
save: "保存專案", // Save Project
|
save: "保存專案", // Save Project
|
||||||
sourceConnection: {
|
sourceConnection: {
|
||||||
title: "來源連線", // Source Connection
|
title: "來源連線", // Source Connection
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ export default class MockFactory {
|
|||||||
id: `project-${name}`,
|
id: `project-${name}`,
|
||||||
name: `Project ${name}`,
|
name: `Project ${name}`,
|
||||||
version: appInfo.version,
|
version: appInfo.version,
|
||||||
|
useSecurityToken: true,
|
||||||
securityToken: `Security-Token-${name}`,
|
securityToken: `Security-Token-${name}`,
|
||||||
assets: {},
|
assets: {},
|
||||||
exportFormat: MockFactory.exportFormat(),
|
exportFormat: MockFactory.exportFormat(),
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ export interface IAppStrings {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
},
|
},
|
||||||
|
useSecurityToken: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
},
|
||||||
save: string;
|
save: string;
|
||||||
sourceConnection: {
|
sourceConnection: {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function encodeFileURI(path: string, additionalEncodings?: boolean): stri
|
|||||||
const encodings = {
|
const encodings = {
|
||||||
"\#": "%23",
|
"\#": "%23",
|
||||||
"\?": "%3F",
|
"\?": "%3F",
|
||||||
};
|
};
|
||||||
const encodedURI = `file:${encodeURI(normalizeSlashes(path))}`;
|
const encodedURI = `file:${encodeURI(normalizeSlashes(path))}`;
|
||||||
if (additionalEncodings) {
|
if (additionalEncodings) {
|
||||||
return encodedURI.replace(matchString, (match) => encodings[match]);
|
return encodedURI.replace(matchString, (match) => encodings[match]);
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ export interface IProject {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
securityToken: string;
|
useSecurityToken: boolean;
|
||||||
|
securityToken?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags: ITag[];
|
tags: ITag[];
|
||||||
sourceConnection: IConnection;
|
sourceConnection: IConnection;
|
||||||
|
|||||||
@@ -6,11 +6,6 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[^\\\\\\\\/:*?\\\\\\\"<>|]*$"
|
"pattern": "^[^\\\\\\\\/:*?\\\\\\\"<>|]*$"
|
||||||
},
|
},
|
||||||
"securityToken": {
|
|
||||||
"title": "${strings.projectSettings.securityToken.title}",
|
|
||||||
"description": "${strings.projectSettings.securityToken.description}",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"sourceConnection": {
|
"sourceConnection": {
|
||||||
"title": "${strings.projectSettings.sourceConnection.title}",
|
"title": "${strings.projectSettings.sourceConnection.title}",
|
||||||
"description": "${strings.projectSettings.sourceConnection.description}",
|
"description": "${strings.projectSettings.sourceConnection.description}",
|
||||||
@@ -44,6 +39,41 @@
|
|||||||
"tags": {
|
"tags": {
|
||||||
"title": "${strings.tags.title}",
|
"title": "${strings.tags.title}",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
},
|
||||||
|
"useSecurityToken": {
|
||||||
|
"title": "${strings.projectSettings.useSecurityToken.title}",
|
||||||
|
"description": "${strings.projectSettings.useSecurityToken.description}",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"useSecurityToken": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"useSecurityToken": {
|
||||||
|
"enum": [
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"securityToken": {
|
||||||
|
"title": "${strings.projectSettings.securityToken.title}",
|
||||||
|
"description": "${strings.projectSettings.securityToken.description}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"useSecurityToken": {
|
||||||
|
"enum": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -263,8 +263,8 @@ describe("Project Form Component", () => {
|
|||||||
onCancel: onCancelHandler,
|
onCancel: onCancelHandler,
|
||||||
});
|
});
|
||||||
const newTagName = "My new tag";
|
const newTagName = "My new tag";
|
||||||
wrapper.find("input").last().simulate("change", { target: { value: newTagName } });
|
wrapper.find("input#tagInputField").last().simulate("change", { target: { value: newTagName } });
|
||||||
wrapper.find("input").last().simulate("keyDown", { keyCode: 13 });
|
wrapper.find("input#tagInputField").last().simulate("keyDown", { keyCode: 13 });
|
||||||
|
|
||||||
const tags = wrapper.state().formData.tags;
|
const tags = wrapper.state().formData.tags;
|
||||||
expect(tags).toHaveLength(1);
|
expect(tags).toHaveLength(1);
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { addLocValues, strings } from "../../../../common/strings";
|
|||||||
import { IConnection, IProject, ITag, IAppSettings } from "../../../../models/applicationState";
|
import { IConnection, IProject, ITag, IAppSettings } from "../../../../models/applicationState";
|
||||||
import { StorageProviderFactory } from "../../../../providers/storage/storageProviderFactory";
|
import { StorageProviderFactory } from "../../../../providers/storage/storageProviderFactory";
|
||||||
import { ConnectionPickerWithRouter } from "../../common/connectionPicker/connectionPicker";
|
import { ConnectionPickerWithRouter } from "../../common/connectionPicker/connectionPicker";
|
||||||
import { CustomField } from "../../common/customField/customField";
|
import { CustomField, CustomWidget } from "../../common/customField/customField";
|
||||||
import CustomFieldTemplate from "../../common/customField/customFieldTemplate";
|
import CustomFieldTemplate from "../../common/customField/customFieldTemplate";
|
||||||
import { ISecurityTokenPickerProps, SecurityTokenPicker } from "../../common/securityTokenPicker/securityTokenPicker";
|
import { ISecurityTokenPickerProps, SecurityTokenPicker } from "../../common/securityTokenPicker/securityTokenPicker";
|
||||||
import "vott-react/dist/css/tagsInput.css";
|
import "vott-react/dist/css/tagsInput.css";
|
||||||
|
import Checkbox from "rc-checkbox";
|
||||||
import { IConnectionProviderPickerProps } from "../../common/connectionProviderPicker/connectionProviderPicker";
|
import { IConnectionProviderPickerProps } from "../../common/connectionProviderPicker/connectionProviderPicker";
|
||||||
import LocalFolderPicker from "../../common/localFolderPicker/localFolderPicker";
|
import LocalFolderPicker from "../../common/localFolderPicker/localFolderPicker";
|
||||||
|
|
||||||
@@ -54,6 +55,11 @@ export interface IProjectFormState {
|
|||||||
export default class ProjectForm extends React.Component<IProjectFormProps, IProjectFormState> {
|
export default class ProjectForm extends React.Component<IProjectFormProps, IProjectFormState> {
|
||||||
private widgets = {
|
private widgets = {
|
||||||
localFolderPicker: (LocalFolderPicker as any) as Widget,
|
localFolderPicker: (LocalFolderPicker as any) as Widget,
|
||||||
|
checkbox: CustomWidget(Checkbox, (props) => ({
|
||||||
|
checked: props.value,
|
||||||
|
onChange: (value) => props.onChange(value.target.checked),
|
||||||
|
disabled: props.disabled,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
private tagsInput: React.RefObject<TagsInput>;
|
private tagsInput: React.RefObject<TagsInput>;
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
"securityToken": {
|
"securityToken": {
|
||||||
"ui:field": "securityToken"
|
"ui:field": "securityToken"
|
||||||
},
|
},
|
||||||
|
"useSecurityToken": {
|
||||||
|
"ui:widget": "checkbox"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"ui:widget": "textarea"
|
"ui:widget": "textarea"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ describe("Project settings page", () => {
|
|||||||
const store = createReduxStore(MockFactory.initialState());
|
const store = createReduxStore(MockFactory.initialState());
|
||||||
const props = MockFactory.projectSettingsProps();
|
const props = MockFactory.projectSettingsProps();
|
||||||
const saveProjectSpy = jest.spyOn(props.projectActions, "saveProject");
|
const saveProjectSpy = jest.spyOn(props.projectActions, "saveProject");
|
||||||
|
const ensureSecurityTokenSpy = jest.spyOn(props.applicationActions, "ensureSecurityToken");
|
||||||
|
|
||||||
projectServiceMock.prototype.save = jest.fn((project) => Promise.resolve(project));
|
projectServiceMock.prototype.save = jest.fn((project) => Promise.resolve(project));
|
||||||
|
|
||||||
@@ -68,6 +69,7 @@ describe("Project settings page", () => {
|
|||||||
await MockFactory.flushUi();
|
await MockFactory.flushUi();
|
||||||
|
|
||||||
expect(saveProjectSpy).toBeCalled();
|
expect(saveProjectSpy).toBeCalled();
|
||||||
|
expect(ensureSecurityTokenSpy).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Throws an error when a user tries to create a duplicate project", async () => {
|
it("Throws an error when a user tries to create a duplicate project", async () => {
|
||||||
@@ -114,6 +116,7 @@ describe("Project settings page", () => {
|
|||||||
const store = createReduxStore(initialState);
|
const store = createReduxStore(initialState);
|
||||||
const props = MockFactory.projectSettingsProps();
|
const props = MockFactory.projectSettingsProps();
|
||||||
const saveProjectSpy = jest.spyOn(props.projectActions, "saveProject");
|
const saveProjectSpy = jest.spyOn(props.projectActions, "saveProject");
|
||||||
|
const ensureSecurityTokenSpy = jest.spyOn(props.applicationActions, "ensureSecurityToken");
|
||||||
const saveAppSettingsSpy = jest.spyOn(props.applicationActions, "saveAppSettings");
|
const saveAppSettingsSpy = jest.spyOn(props.applicationActions, "saveAppSettings");
|
||||||
|
|
||||||
projectServiceMock.prototype.save = jest.fn((project) => Promise.resolve(project));
|
projectServiceMock.prototype.save = jest.fn((project) => Promise.resolve(project));
|
||||||
@@ -133,6 +136,11 @@ describe("Project settings page", () => {
|
|||||||
securityToken: `${project.name} Token`,
|
securityToken: `${project.name} Token`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(ensureSecurityTokenSpy).toBeCalledWith({
|
||||||
|
...project,
|
||||||
|
securityToken: `${project.name} Token`,
|
||||||
|
});
|
||||||
|
|
||||||
expect(localStorage.removeItem).toBeCalledWith("projectForm");
|
expect(localStorage.removeItem).toBeCalledWith("projectForm");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function mapDispatchToProps(dispatch) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectFormTempKey = "projectForm";
|
const projectFormKey = "projectForm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name - Project Settings Page
|
* @name - Project Settings Page
|
||||||
@@ -64,14 +64,17 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
|
|||||||
// If we are creating a new project check to see if there is a partial
|
// If we are creating a new project check to see if there is a partial
|
||||||
// project already created in local storage
|
// project already created in local storage
|
||||||
if (this.props.match.url === "/projects/create") {
|
if (this.props.match.url === "/projects/create") {
|
||||||
const projectJson = localStorage.getItem(projectFormTempKey);
|
const projectJson = localStorage.getItem(projectFormKey);
|
||||||
if (projectJson) {
|
if (projectJson) {
|
||||||
this.setState({ project: JSON.parse(projectJson) });
|
this.setState({ project: JSON.parse(projectJson) });
|
||||||
}
|
}
|
||||||
} else if (!this.props.project && projectId) {
|
} else if (!this.props.project && projectId) {
|
||||||
const projectToLoad = this.props.recentProjects.find((project) => project.id === projectId);
|
const projectToLoad = this.props.recentProjects.find((project) => project.id === projectId);
|
||||||
if (projectToLoad) {
|
if (projectToLoad) {
|
||||||
await this.props.applicationActions.ensureSecurityToken(projectToLoad);
|
if (projectToLoad.useSecurityToken) {
|
||||||
|
await this.props.applicationActions.ensureSecurityToken(projectToLoad);
|
||||||
|
}
|
||||||
|
|
||||||
await this.props.projectActions.loadProject(projectToLoad);
|
await this.props.projectActions.loadProject(projectToLoad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,16 +122,19 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
|
|||||||
*/
|
*/
|
||||||
private onFormChange = (project: IProject) => {
|
private onFormChange = (project: IProject) => {
|
||||||
if (this.isPartialProject(project)) {
|
if (this.isPartialProject(project)) {
|
||||||
localStorage.setItem(projectFormTempKey, JSON.stringify(project));
|
localStorage.setItem(projectFormKey, JSON.stringify(project));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onFormSubmit = async (project: IProject) => {
|
private onFormSubmit = async (project: IProject) => {
|
||||||
const isNew = !(!!project.id);
|
const isNew = !(!!project.id);
|
||||||
|
|
||||||
await this.props.applicationActions.ensureSecurityToken(project);
|
if (project.useSecurityToken) {
|
||||||
|
await this.props.applicationActions.ensureSecurityToken(project);
|
||||||
|
}
|
||||||
|
|
||||||
await this.props.projectActions.saveProject(project);
|
await this.props.projectActions.saveProject(project);
|
||||||
localStorage.removeItem(projectFormTempKey);
|
localStorage.removeItem(projectFormKey);
|
||||||
|
|
||||||
toast.success(interpolate(strings.projectSettings.messages.saveSuccess, { project }));
|
toast.success(interpolate(strings.projectSettings.messages.saveSuccess, { project }));
|
||||||
|
|
||||||
@@ -140,7 +146,7 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onFormCancel = () => {
|
private onFormCancel = () => {
|
||||||
localStorage.removeItem(projectFormTempKey);
|
localStorage.removeItem(projectFormKey);
|
||||||
this.props.history.goBack();
|
this.props.history.goBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,9 @@ import {
|
|||||||
IProject,
|
IProject,
|
||||||
} from "../../models/applicationState";
|
} from "../../models/applicationState";
|
||||||
import { createAction, createPayloadAction, IPayloadAction } from "./actionCreators";
|
import { createAction, createPayloadAction, IPayloadAction } from "./actionCreators";
|
||||||
import { ExportAssetState, IExportResults } from "../../providers/export/exportProvider";
|
import { IExportResults } from "../../providers/export/exportProvider";
|
||||||
import { appInfo } from "../../common/appInfo";
|
import { appInfo } from "../../common/appInfo";
|
||||||
import { strings } from "../../common/strings";
|
import { strings } from "../../common/strings";
|
||||||
import { IExportFormat } from "vott-react";
|
|
||||||
import { IVottJsonExportProviderOptions } from "../../providers/export/vottJson";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actions to be performed in relation to projects
|
* Actions to be performed in relation to projects
|
||||||
@@ -48,7 +46,7 @@ export function loadProject(project: IProject):
|
|||||||
const projectToken = appState.appSettings.securityTokens
|
const projectToken = appState.appSettings.securityTokens
|
||||||
.find((securityToken) => securityToken.name === project.securityToken);
|
.find((securityToken) => securityToken.name === project.securityToken);
|
||||||
|
|
||||||
if (!projectToken) {
|
if (project.useSecurityToken && !projectToken) {
|
||||||
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
|
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
|
||||||
}
|
}
|
||||||
const loadedProject = await projectService.load(project, projectToken);
|
const loadedProject = await projectService.load(project, projectToken);
|
||||||
@@ -76,7 +74,7 @@ export function saveProject(project: IProject)
|
|||||||
const projectToken = appState.appSettings.securityTokens
|
const projectToken = appState.appSettings.securityTokens
|
||||||
.find((securityToken) => securityToken.name === project.securityToken);
|
.find((securityToken) => securityToken.name === project.securityToken);
|
||||||
|
|
||||||
if (!projectToken) {
|
if (project.useSecurityToken && !projectToken) {
|
||||||
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
|
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +102,7 @@ export function deleteProject(project: IProject)
|
|||||||
const projectToken = appState.appSettings.securityTokens
|
const projectToken = appState.appSettings.securityTokens
|
||||||
.find((securityToken) => securityToken.name === project.securityToken);
|
.find((securityToken) => securityToken.name === project.securityToken);
|
||||||
|
|
||||||
if (!projectToken) {
|
if (project.useSecurityToken && !projectToken) {
|
||||||
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
|
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export default class ImportService implements IImportService {
|
|||||||
id: shortid.generate(),
|
id: shortid.generate(),
|
||||||
name: projectInfo.file.name.split(".")[0],
|
name: projectInfo.file.name.split(".")[0],
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
|
useSecurityToken: true,
|
||||||
securityToken: `${projectInfo.file.name.split(".")[0]} Token`,
|
securityToken: `${projectInfo.file.name.split(".")[0]} Token`,
|
||||||
description: "Converted V1 Project",
|
description: "Converted V1 Project",
|
||||||
tags: parsedTags,
|
tags: parsedTags,
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import {
|
|||||||
import { constants } from "../common/constants";
|
import { constants } from "../common/constants";
|
||||||
import { ExportProviderFactory } from "../providers/export/exportProviderFactory";
|
import { ExportProviderFactory } from "../providers/export/exportProviderFactory";
|
||||||
import { generateKey } from "../common/crypto";
|
import { generateKey } from "../common/crypto";
|
||||||
import { encryptProject, decryptProject } from "../common/utils";
|
import * as utils from "../common/utils";
|
||||||
import { ExportAssetState } from "../providers/export/exportProvider";
|
import { ExportAssetState } from "../providers/export/exportProvider";
|
||||||
import { IVottJsonExportProviderOptions } from "../providers/export/vottJson";
|
import { IVottJsonExportProviderOptions } from "../providers/export/vottJson";
|
||||||
import { IPascalVOCExportProviderOptions } from "../providers/export/pascalVOC";
|
import { IPascalVOCExportProviderOptions } from "../providers/export/pascalVOC";
|
||||||
|
|
||||||
describe("Project Service", () => {
|
describe("Project Service", () => {
|
||||||
let projectSerivce: IProjectService = null;
|
let projectService: IProjectService = null;
|
||||||
let testProject: IProject = null;
|
let testProject: IProject = null;
|
||||||
let projectList: IProject[] = null;
|
let projectList: IProject[] = null;
|
||||||
let securityToken: ISecurityToken = null;
|
let securityToken: ISecurityToken = null;
|
||||||
@@ -33,27 +33,41 @@ describe("Project Service", () => {
|
|||||||
StorageProviderFactory.create = jest.fn(() => storageProviderMock);
|
StorageProviderFactory.create = jest.fn(() => storageProviderMock);
|
||||||
ExportProviderFactory.create = jest.fn(() => exportProviderMock);
|
ExportProviderFactory.create = jest.fn(() => exportProviderMock);
|
||||||
|
|
||||||
|
const encryptSpy = jest.spyOn(utils, "encryptProject");
|
||||||
|
const decryptSpy = jest.spyOn(utils, "decryptProject");
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
securityToken = {
|
securityToken = {
|
||||||
name: "TestToken",
|
name: "TestToken",
|
||||||
key: generateKey(),
|
key: generateKey(),
|
||||||
};
|
};
|
||||||
testProject = MockFactory.createTestProject("TestProject");
|
testProject = MockFactory.createTestProject("TestProject");
|
||||||
projectSerivce = new ProjectService();
|
projectService = new ProjectService();
|
||||||
|
|
||||||
storageProviderMock.writeText.mockClear();
|
storageProviderMock.writeText.mockClear();
|
||||||
storageProviderMock.deleteFile.mockClear();
|
storageProviderMock.deleteFile.mockClear();
|
||||||
|
encryptSpy.mockClear();
|
||||||
|
decryptSpy.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Load decrypts any project settings using the specified key", async () => {
|
it("Load decrypts any project settings using the specified key", async () => {
|
||||||
const encryptedProject = encryptProject(testProject, securityToken);
|
const encryptedProject = utils.encryptProject(testProject, securityToken);
|
||||||
const decryptedProject = await projectSerivce.load(encryptedProject, securityToken);
|
const decryptedProject = await projectService.load(encryptedProject, securityToken);
|
||||||
|
|
||||||
expect(decryptedProject).toEqual(testProject);
|
expect(decryptedProject).toEqual(testProject);
|
||||||
|
expect(decryptSpy).toBeCalledWith(encryptedProject, securityToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Does not decrypt project when a security token is not in use", async () => {
|
||||||
|
const project: IProject = { ...testProject, useSecurityToken: false };
|
||||||
|
|
||||||
|
const loadedProject = await projectService.load(project);
|
||||||
|
expect(loadedProject).toEqual(project);
|
||||||
|
expect(decryptSpy).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Saves calls project storage provider to write project", async () => {
|
it("Saves calls project storage provider to write project", async () => {
|
||||||
const result = await projectSerivce.save(testProject, securityToken);
|
const result = await projectService.save(testProject, securityToken);
|
||||||
|
|
||||||
const encryptedProject: IProject = {
|
const encryptedProject: IProject = {
|
||||||
...testProject,
|
...testProject,
|
||||||
@@ -72,6 +86,7 @@ describe("Project Service", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
expect(result).toEqual(encryptedProject);
|
expect(result).toEqual(encryptedProject);
|
||||||
|
expect(encryptSpy).toBeCalledWith(testProject, securityToken);
|
||||||
expect(StorageProviderFactory.create).toBeCalledWith(
|
expect(StorageProviderFactory.create).toBeCalledWith(
|
||||||
testProject.targetConnection.providerType,
|
testProject.targetConnection.providerType,
|
||||||
testProject.targetConnection.providerOptions,
|
testProject.targetConnection.providerOptions,
|
||||||
@@ -82,9 +97,16 @@ describe("Project Service", () => {
|
|||||||
expect.any(String));
|
expect.any(String));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Does not encrypt project during save when a security token is not in use", async () => {
|
||||||
|
const projectToSave: IProject = { ...testProject, useSecurityToken: false };
|
||||||
|
await projectService.save(projectToSave);
|
||||||
|
|
||||||
|
expect(encryptSpy).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("sets default export settings when not defined", async () => {
|
it("sets default export settings when not defined", async () => {
|
||||||
testProject.exportFormat = null;
|
testProject.exportFormat = null;
|
||||||
const result = await projectSerivce.save(testProject, securityToken);
|
const result = await projectService.save(testProject, securityToken);
|
||||||
|
|
||||||
const vottJsonExportProviderOptions: IVottJsonExportProviderOptions = {
|
const vottJsonExportProviderOptions: IVottJsonExportProviderOptions = {
|
||||||
assetState: ExportAssetState.Visited,
|
assetState: ExportAssetState.Visited,
|
||||||
@@ -96,14 +118,14 @@ describe("Project Service", () => {
|
|||||||
providerOptions: vottJsonExportProviderOptions,
|
providerOptions: vottJsonExportProviderOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptedProject = decryptProject(result, securityToken);
|
const decryptedProject = utils.decryptProject(result, securityToken);
|
||||||
|
|
||||||
expect(decryptedProject.exportFormat).toEqual(expectedExportFormat);
|
expect(decryptedProject.exportFormat).toEqual(expectedExportFormat);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets default active learning setting when not defined", async () => {
|
it("sets default active learning setting when not defined", async () => {
|
||||||
testProject.activeLearningSettings = null;
|
testProject.activeLearningSettings = null;
|
||||||
const result = await projectSerivce.save(testProject, securityToken);
|
const result = await projectService.save(testProject, securityToken);
|
||||||
|
|
||||||
const activeLearningSettings: IActiveLearningSettings = {
|
const activeLearningSettings: IActiveLearningSettings = {
|
||||||
autoDetect: false,
|
autoDetect: false,
|
||||||
@@ -116,7 +138,7 @@ describe("Project Service", () => {
|
|||||||
|
|
||||||
it("initializes tags to empty array if not defined", async () => {
|
it("initializes tags to empty array if not defined", async () => {
|
||||||
testProject.tags = null;
|
testProject.tags = null;
|
||||||
const result = await projectSerivce.save(testProject, securityToken);
|
const result = await projectService.save(testProject, securityToken);
|
||||||
|
|
||||||
expect(result.tags).toEqual([]);
|
expect(result.tags).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -127,7 +149,7 @@ describe("Project Service", () => {
|
|||||||
providerOptions: null,
|
providerOptions: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
await projectSerivce.save(testProject, securityToken);
|
await projectService.save(testProject, securityToken);
|
||||||
|
|
||||||
expect(ExportProviderFactory.create).toBeCalledWith(
|
expect(ExportProviderFactory.create).toBeCalledWith(
|
||||||
testProject.exportFormat.providerType,
|
testProject.exportFormat.providerType,
|
||||||
@@ -140,7 +162,7 @@ describe("Project Service", () => {
|
|||||||
it("Save throws error if writing to storage provider fails", async () => {
|
it("Save throws error if writing to storage provider fails", async () => {
|
||||||
const expectedError = "Error writing to storage provider";
|
const expectedError = "Error writing to storage provider";
|
||||||
storageProviderMock.writeText.mockImplementationOnce(() => Promise.reject(expectedError));
|
storageProviderMock.writeText.mockImplementationOnce(() => Promise.reject(expectedError));
|
||||||
await expect(projectSerivce.save(testProject, securityToken)).rejects.toEqual(expectedError);
|
await expect(projectService.save(testProject, securityToken)).rejects.toEqual(expectedError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Save throws error if storage provider cannot be created", async () => {
|
it("Save throws error if storage provider cannot be created", async () => {
|
||||||
@@ -148,11 +170,11 @@ describe("Project Service", () => {
|
|||||||
const createMock = StorageProviderFactory.create as jest.Mock;
|
const createMock = StorageProviderFactory.create as jest.Mock;
|
||||||
createMock.mockImplementationOnce(() => { throw expectedError; });
|
createMock.mockImplementationOnce(() => { throw expectedError; });
|
||||||
|
|
||||||
await expect(projectSerivce.save(testProject, securityToken)).rejects.toEqual(expectedError);
|
await expect(projectService.save(testProject, securityToken)).rejects.toEqual(expectedError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Delete calls project storage provider to delete project", async () => {
|
it("Delete calls project storage provider to delete project", async () => {
|
||||||
await projectSerivce.delete(testProject);
|
await projectService.delete(testProject);
|
||||||
|
|
||||||
expect(StorageProviderFactory.create).toBeCalledWith(
|
expect(StorageProviderFactory.create).toBeCalledWith(
|
||||||
testProject.targetConnection.providerType,
|
testProject.targetConnection.providerType,
|
||||||
@@ -167,7 +189,7 @@ describe("Project Service", () => {
|
|||||||
storageProviderMock.deleteFile
|
storageProviderMock.deleteFile
|
||||||
.mockImplementationOnce(() => Promise.reject(expectedError));
|
.mockImplementationOnce(() => Promise.reject(expectedError));
|
||||||
|
|
||||||
await expect(projectSerivce.delete(testProject)).rejects.toEqual(expectedError);
|
await expect(projectService.delete(testProject)).rejects.toEqual(expectedError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Delete call fails if storage provider cannot be created", async () => {
|
it("Delete call fails if storage provider cannot be created", async () => {
|
||||||
@@ -175,20 +197,20 @@ describe("Project Service", () => {
|
|||||||
const createMock = StorageProviderFactory.create as jest.Mock;
|
const createMock = StorageProviderFactory.create as jest.Mock;
|
||||||
createMock.mockImplementationOnce(() => { throw expectedError; });
|
createMock.mockImplementationOnce(() => { throw expectedError; });
|
||||||
|
|
||||||
await expect(projectSerivce.delete(testProject)).rejects.toEqual(expectedError);
|
await expect(projectService.delete(testProject)).rejects.toEqual(expectedError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("isDuplicate returns false when called with a unique project", async () => {
|
it("isDuplicate returns false when called with a unique project", async () => {
|
||||||
testProject = MockFactory.createTestProject("TestProject");
|
testProject = MockFactory.createTestProject("TestProject");
|
||||||
projectList = MockFactory.createTestProjects();
|
projectList = MockFactory.createTestProjects();
|
||||||
expect(projectSerivce.isDuplicate(testProject, projectList)).toEqual(false);
|
expect(projectService.isDuplicate(testProject, projectList)).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("isDuplicate returns true when called with a duplicate project", async () => {
|
it("isDuplicate returns true when called with a duplicate project", async () => {
|
||||||
testProject = MockFactory.createTestProject("1");
|
testProject = MockFactory.createTestProject("1");
|
||||||
testProject.id = undefined;
|
testProject.id = undefined;
|
||||||
projectList = MockFactory.createTestProjects();
|
projectList = MockFactory.createTestProjects();
|
||||||
expect(projectSerivce.isDuplicate(testProject, projectList)).toEqual(true);
|
expect(projectService.isDuplicate(testProject, projectList)).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("deletes all asset metadata files when project is deleted", async () => {
|
it("deletes all asset metadata files when project is deleted", async () => {
|
||||||
@@ -199,7 +221,7 @@ describe("Project Service", () => {
|
|||||||
|
|
||||||
testProject.assets = _.keyBy(assets, (asset) => asset.id);
|
testProject.assets = _.keyBy(assets, (asset) => asset.id);
|
||||||
|
|
||||||
await projectSerivce.delete(testProject);
|
await projectService.delete(testProject);
|
||||||
expect(storageProviderMock.deleteFile.mock.calls).toHaveLength(assets.length + 1);
|
expect(storageProviderMock.deleteFile.mock.calls).toHaveLength(assets.length + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -215,8 +237,8 @@ describe("Project Service", () => {
|
|||||||
} as IPascalVOCExportProviderOptions,
|
} as IPascalVOCExportProviderOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
const encryptedProject = encryptProject(testProject, securityToken);
|
const encryptedProject = utils.encryptProject(testProject, securityToken);
|
||||||
const decryptedProject = await projectSerivce.load(encryptedProject, securityToken);
|
const decryptedProject = await projectService.load(encryptedProject, securityToken);
|
||||||
|
|
||||||
expect(decryptedProject.exportFormat.providerType).toEqual("pascalVOC");
|
expect(decryptedProject.exportFormat.providerType).toEqual("pascalVOC");
|
||||||
expect(decryptedProject.exportFormat.providerOptions).toEqual(testProject.exportFormat.providerOptions);
|
expect(decryptedProject.exportFormat.providerOptions).toEqual(testProject.exportFormat.providerOptions);
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import { IExportFormat } from "vott-react";
|
|||||||
* @member delete - Delete a project
|
* @member delete - Delete a project
|
||||||
*/
|
*/
|
||||||
export interface IProjectService {
|
export interface IProjectService {
|
||||||
load(project: IProject, securityToken: ISecurityToken): Promise<IProject>;
|
load(project: IProject, securityToken?: ISecurityToken): Promise<IProject>;
|
||||||
save(project: IProject, securityToken: ISecurityToken): Promise<IProject>;
|
save(project: IProject, securityToken?: ISecurityToken): Promise<IProject>;
|
||||||
delete(project: IProject): Promise<void>;
|
delete(project: IProject): Promise<void>;
|
||||||
isDuplicate(project: IProject, projectList: IProject[]): boolean;
|
isDuplicate(project: IProject, projectList: IProject[]): boolean;
|
||||||
}
|
}
|
||||||
@@ -49,11 +49,13 @@ export default class ProjectService implements IProjectService {
|
|||||||
* @param project The project JSON to load
|
* @param project The project JSON to load
|
||||||
* @param securityToken The security token used to decrypt sensitive project settings
|
* @param securityToken The security token used to decrypt sensitive project settings
|
||||||
*/
|
*/
|
||||||
public load(project: IProject, securityToken: ISecurityToken): Promise<IProject> {
|
public load(project: IProject, securityToken?: ISecurityToken): Promise<IProject> {
|
||||||
Guard.null(project);
|
Guard.null(project);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loadedProject = decryptProject(project, securityToken);
|
const loadedProject = project.useSecurityToken
|
||||||
|
? decryptProject(project, securityToken)
|
||||||
|
: { ...project };
|
||||||
|
|
||||||
// Ensure tags is always initialized to an array
|
// Ensure tags is always initialized to an array
|
||||||
if (!loadedProject.tags) {
|
if (!loadedProject.tags) {
|
||||||
@@ -84,7 +86,7 @@ export default class ProjectService implements IProjectService {
|
|||||||
* @param project - Project to save
|
* @param project - Project to save
|
||||||
* @param securityToken - Security Token to encrypt
|
* @param securityToken - Security Token to encrypt
|
||||||
*/
|
*/
|
||||||
public async save(project: IProject, securityToken: ISecurityToken): Promise<IProject> {
|
public async save(project: IProject, securityToken?: ISecurityToken): Promise<IProject> {
|
||||||
Guard.null(project);
|
Guard.null(project);
|
||||||
|
|
||||||
if (!project.id) {
|
if (!project.id) {
|
||||||
@@ -110,7 +112,9 @@ export default class ProjectService implements IProjectService {
|
|||||||
|
|
||||||
const storageProvider = StorageProviderFactory.createFromConnection(project.targetConnection);
|
const storageProvider = StorageProviderFactory.createFromConnection(project.targetConnection);
|
||||||
await this.saveExportSettings(project);
|
await this.saveExportSettings(project);
|
||||||
project = encryptProject(project, securityToken);
|
project = project.useSecurityToken
|
||||||
|
? encryptProject(project, securityToken)
|
||||||
|
: { ...project };
|
||||||
|
|
||||||
await storageProvider.writeText(
|
await storageProvider.writeText(
|
||||||
`${project.name}${constants.projectFileExtension}`,
|
`${project.name}${constants.projectFileExtension}`,
|
||||||
|
|||||||
Referência em uma Nova Issue
Bloquear um usuário