Skip to content

Commit 149d26d

Browse files
🌟 Can upload a configuration to Configuration Hub
1 parent ce124a2 commit 149d26d

File tree

13 files changed

+286
-19
lines changed

13 files changed

+286
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This changelog is following the recommended format by [keepachangelog](https://k
1111
### Added
1212

1313
- Can upload entitlements for a Delimited file
14+
- Can upload a configuration to Configuration Hub
1415

1516
### Changed
1617

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@
117117
"title": "Export sp-config...",
118118
"icon": "$(export)"
119119
},
120+
{
121+
"command": "vscode-sailpoint-identitynow.upload-config.view",
122+
"title": "Upload config to Configuration Hub...",
123+
"icon": "$(import)"
124+
},
120125
{
121126
"command": "vscode-sailpoint-identitynow.import-config.view",
122127
"title": "Import config...",
@@ -672,6 +677,10 @@
672677
"command": "vscode-sailpoint-identitynow.export-node-config.view",
673678
"when": "never"
674679
},
680+
{
681+
"command": "vscode-sailpoint-identitynow.upload-config.view",
682+
"when": "never"
683+
},
675684
{
676685
"command": "vscode-sailpoint-identitynow.import-config.view",
677686
"when": "never"
@@ -1091,6 +1100,11 @@
10911100
"when": "view == vscode-sailpoint-identitynow.view && viewItem =~ /^tenant/",
10921101
"group": "config"
10931102
},
1103+
{
1104+
"command": "vscode-sailpoint-identitynow.upload-config.view",
1105+
"when": "view == vscode-sailpoint-identitynow.view && viewItem =~ /^tenant/",
1106+
"group": "config"
1107+
},
10941108
{
10951109
"command": "vscode-sailpoint-identitynow.import-config.view",
10961110
"when": "view == vscode-sailpoint-identitynow.view && viewItem =~ /^tenant/",

src/commands/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ export const REMOVE_TENANT = `${COMMAND_PREFIX}.remove-tenant`;
1212
export const EXPORT_CONFIG_VIEW = `${COMMAND_PREFIX}.export-config.view`;
1313
export const EXPORT_CONFIG_PALETTE = `${COMMAND_PREFIX}.export-config.palette`;
1414
export const EXPORT_NODE_CONFIG_VIEW = `${COMMAND_PREFIX}.export-node-config.view`;
15+
1516
export const IMPORT_CONFIG_VIEW = `${COMMAND_PREFIX}.import-config.view`;
1617
export const IMPORT_CONFIG_PALETTE = `${COMMAND_PREFIX}.import-config.palette`;
1718
export const IMPORT_CONFIG_MENU = `${COMMAND_PREFIX}.import-config.menu`;
19+
20+
export const UPLOAD_CONFIGURATION_VIEW = `${COMMAND_PREFIX}.upload-config.view`;
21+
1822
export const AGGREGATE = `${COMMAND_PREFIX}.aggregate-source`;
1923
export const AGGREGATE_DISABLE_OPTIMIZATION = `${COMMAND_PREFIX}.aggregate-source-disable-optimization`;
2024
export const AGGREGATE_ENTITLEMENTS = `${COMMAND_PREFIX}.aggregate-entitlements`;

src/commands/spconfig-import/SPConfigImporter.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import * as vscode from 'vscode';
22
import { ISCClient } from '../../services/ISCClient';
3-
import { delay } from '../../utils';
43
import { IMPORTABLE_OBJECT_TYPE_ITEMS } from '../../models/ObjectTypeQuickPickItem';
5-
import { ImportOptionsBeta, SpConfigJobBetaStatusBeta } from 'sailpoint-api-client';
4+
import { ImportOptionsBeta } from 'sailpoint-api-client';
65
import { ImportJobResults } from '../../models/JobStatus';
6+
import { waitForImportJob } from './utils';
77

88
/**
99
* Base class for all importer
1010
*/
1111
export class SPConfigImporter {
12-
private client!: ISCClient;
12+
private client: ISCClient;
1313

1414
constructor(
1515
private readonly tenantId: string,
@@ -32,13 +32,7 @@ export class SPConfigImporter {
3232
cancellable: false
3333
}, async (task, token) => {
3434
const jobId = await this.client.startImportJob(this.data, this.importOptions);
35-
let jobStatus: any;
36-
do {
37-
await delay(1000);
38-
jobStatus = await this.client.getImportJobStatus(jobId);
39-
console.log({ jobStatus });
40-
} while (jobStatus.status === SpConfigJobBetaStatusBeta.NotStarted || jobStatus.status === SpConfigJobBetaStatusBeta.InProgress);
41-
35+
const jobStatus =await waitForImportJob(this.client, jobId, token)
4236
const importJobresult = await this.client.getImportJobResult(jobId);
4337
const result = { ...importJobresult, ...jobStatus };
4438
return result;
@@ -50,7 +44,7 @@ export class SPConfigImporter {
5044
for (objectType in importJobresult.results) {
5145
importJobresult.results[objectType]?.errors
5246
.forEach((element) => {
53-
errors.push(element.detail.exceptionMessage ?? element.text);
47+
errors.push(element.details.exceptionMessage ?? element.text);
5448
});
5549
}
5650

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as vscode from 'vscode';
2+
import * as fs from 'fs';
3+
import { TenantTreeItem } from "../../models/ISCTreeItem";
4+
import { TenantService } from "../../services/TenantService";
5+
import { ISCClient } from '../../services/ISCClient';
6+
import { basename } from 'path';
7+
import { waitForUploadJob } from './utils';
8+
import { WizardContext } from '../../wizard/wizardContext';
9+
import { runWizard } from '../../wizard/wizard';
10+
import { InputPromptStep } from '../../wizard/inputPromptStep';
11+
import { QuickPickTenantStep } from '../../wizard/quickPickTenantStep';
12+
import { ChooseFileStep } from '../../wizard/chooseFileStep';
13+
import { BackupResponseV2024StatusV2024 } from 'sailpoint-api-client';
14+
15+
/**
16+
* Entry point to import file from the tree view. Tenant is already known
17+
* @param node
18+
*/
19+
export class UploadBackupTreeViewCommand {
20+
constructor(
21+
readonly tenantService: TenantService
22+
) { }
23+
24+
/**
25+
* 1. Choose the file
26+
* 2. get content of the file
27+
* 3. Start the import steps
28+
*/
29+
async execute(node?: TenantTreeItem): Promise<void> {
30+
console.log("> UploadBackupTreeViewCommand.execute");
31+
32+
const context: WizardContext = {};
33+
34+
// if the command is called from the Tree View
35+
if (node !== undefined && node instanceof TenantTreeItem) {
36+
context["tenant"] = this.tenantService.getTenant(node.tenantId);
37+
}
38+
39+
const UPLOAD_NAME_KEY = "uploadName",
40+
FILE_URI_KEY = "fileUri",
41+
DATA_KEY = "data"
42+
43+
const values = await runWizard({
44+
title: "Upload a configuration",
45+
hideStepCount: true,
46+
promptSteps: [
47+
new QuickPickTenantStep(
48+
this.tenantService,
49+
async (wizardContext) => { },
50+
"upload a configuration"),
51+
new ChooseFileStep({
52+
name: FILE_URI_KEY,
53+
afterPrompt: async (ctx) => {
54+
const uri = ctx[FILE_URI_KEY]
55+
const data = fs.readFileSync(uri.fsPath).toString();
56+
const spConfig = JSON.parse(data);
57+
ctx[UPLOAD_NAME_KEY] = spConfig.description
58+
ctx[DATA_KEY] = data
59+
},
60+
options: {
61+
canSelectMany: false
62+
}
63+
}),
64+
new InputPromptStep({
65+
name: UPLOAD_NAME_KEY,
66+
displayName: "configuration",
67+
options: {
68+
placeHolder: "Name that will be assigned to the uploaded configuration file.",
69+
shouldPrompt: () => true // always prompt
70+
},
71+
})
72+
73+
]
74+
}, context);
75+
76+
if (values === undefined) { return; }
77+
78+
79+
await vscode.window.withProgress({
80+
location: vscode.ProgressLocation.Notification,
81+
title: `Uploading configuration ${node.tenantDisplayName}...`,
82+
cancellable: false
83+
}, async (task, token) => {
84+
const fileUri = values[FILE_URI_KEY]
85+
const data = values[DATA_KEY]
86+
const filename = basename(fileUri.fsPath)
87+
88+
89+
const client = new ISCClient(node.tenantId, node.tenantName);
90+
const job = await client.uploadBackup(data, filename, values[UPLOAD_NAME_KEY]);
91+
const jobStatus = await waitForUploadJob(client, job.jobId, token)
92+
return jobStatus;
93+
}).then((jobStatus) => {
94+
if (jobStatus.status == BackupResponseV2024StatusV2024.Complete) {
95+
vscode.window.showInformationMessage(`Configuration uploaded successfully to ${node.tenantDisplayName}`)
96+
} else {
97+
vscode.window.showErrorMessage(`Could not upload configuration to ${node.tenantDisplayName}: ${jobStatus.message}`)
98+
}
99+
100+
})
101+
102+
}
103+
}
104+

src/commands/spconfig-import/utils.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { BackupResponseV2024, SpConfigJobBeta, SpConfigJobBetaStatusBeta } from "sailpoint-api-client";
2+
import { ISCClient } from "../../services/ISCClient";
3+
import * as vscode from 'vscode';
4+
import { delay } from "../../utils";
5+
6+
export async function waitForImportJob(client: ISCClient, taskId: string, token: vscode.CancellationToken): Promise<SpConfigJobBeta | null> {
7+
8+
return await waitFor(taskId,
9+
token,
10+
async (taskId) => await client.getImportJobStatus(taskId),
11+
(status: SpConfigJobBeta) => status.status === SpConfigJobBetaStatusBeta.NotStarted || status.status === SpConfigJobBetaStatusBeta.InProgress)
12+
}
13+
14+
export async function waitForUploadJob(client: ISCClient, taskId: string, token: vscode.CancellationToken): Promise<BackupResponseV2024 | null> {
15+
16+
return await waitFor(taskId,
17+
token,
18+
async (taskId) => await client.getUploadConfigurationJobStatus(taskId),
19+
(status: BackupResponseV2024) => status.status === SpConfigJobBetaStatusBeta.NotStarted || status.status === SpConfigJobBetaStatusBeta.InProgress)
20+
}
21+
22+
23+
export async function waitFor<T>(taskId: string, token: vscode.CancellationToken, updateStatus: (taskId) => Promise<T>, isPending: (status: T) => boolean): Promise<T | undefined> {
24+
console.log("> waifFor", taskId);
25+
let jobStatus: T | undefined = undefined;
26+
do {
27+
if (token.isCancellationRequested) {
28+
return null
29+
}
30+
await delay(5000);
31+
if (token.isCancellationRequested) {
32+
return null
33+
}
34+
jobStatus = await updateStatus(taskId)
35+
console.log({ jobStatus });
36+
} while (isPending(jobStatus));
37+
38+
39+
return jobStatus
40+
}

src/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import { CertificationCampaignStatusFilterCommand } from './campaign-webview/Cer
8080
import { CertificationCampaignNameFilterCommand } from './campaign-webview/CertificationCampaignNameFilterCommand';
8181
import { EditServiceDeskTimeCheckConfiguration } from './commands/tenant/editServiceDeskTimeCheckConfiguration';
8282
import { EntitlementImportNodeCommand } from './commands/source/importEntitlements';
83+
import { UploadBackupTreeViewCommand } from './commands/spconfig-import/uploadBackupTreeViewCommand';
8384

8485
// this method is called when your extension is activated
8586
// your extension is activated the very first time the command is executed
@@ -314,6 +315,11 @@ export function activate(context: vscode.ExtensionContext) {
314315
vscode.commands.registerCommand(commands.IMPORT_CONFIG_MENU,
315316
menuImporterCommand.execute, menuImporterCommand));
316317

318+
const uploadBackupTreeViewCommand = new UploadBackupTreeViewCommand(tenantService);
319+
context.subscriptions.push(
320+
vscode.commands.registerCommand(commands.UPLOAD_CONFIGURATION_VIEW,
321+
uploadBackupTreeViewCommand.execute, uploadBackupTreeViewCommand));
322+
317323
const treeviewImporterCommand = new ImportConfigTreeViewCommand(tenantService);
318324
context.subscriptions.push(
319325
vscode.commands.registerCommand(commands.IMPORT_CONFIG_VIEW,

src/services/ISCClient.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { SailPointISCAuthenticationProvider } from "./AuthenticationProvider";
88
import { compareByName, convertToText } from "../utils";
99
import { DEFAULT_ACCOUNTS_QUERY_PARAMS } from "../models/Account";
1010
import { DEFAULT_ENTITLEMENTS_QUERY_PARAMS } from "../models/Entitlements";
11-
import { Configuration, IdentityProfilesApi, IdentityProfile, LifecycleState, LifecycleStatesApi, Paginator, ServiceDeskIntegrationApi, ServiceDeskIntegrationDto, Source, SourcesApi, TransformsApi, WorkflowsBetaApi, WorkflowBeta, WorkflowExecutionBeta, WorkflowLibraryTriggerBeta, ConnectorRuleManagementBetaApi, ConnectorRuleResponseBeta, ConnectorRuleValidationResponseBeta, AccountsApi, AccountsApiListAccountsRequest, Account, EntitlementsBetaApi, EntitlementsBetaApiListEntitlementsRequest, PublicIdentitiesApi, PublicIdentitiesApiGetPublicIdentitiesRequest, PublicIdentity, JsonPatchOperationBeta, SPConfigBetaApi, SpConfigImportResultsBeta, SpConfigJobBeta, ImportOptionsBeta, SpConfigExportResultsBeta, ObjectExportImportOptionsBeta, TransformRead, GovernanceGroupsBetaApi, WorkgroupDtoBeta, AccessProfilesApi, AccessProfilesApiListAccessProfilesRequest, AccessProfile, RolesApi, Role, RolesApiListRolesRequest, Search, SearchApi, IdentityDocument, SearchDocument, AccessProfileDocument, EntitlementDocument, EntitlementBeta, RoleDocument, SourcesBetaApi, StatusResponseBeta, Schema, FormBeta, CustomFormsBetaApi, ExportFormDefinitionsByTenant200ResponseInnerBeta, FormDefinitionResponseBeta, NotificationsBetaApi, TemplateDtoBeta, SegmentsApi, Segment, SearchAttributeConfigurationBetaApi, SearchAttributeConfigBeta, IdentityAttributesBetaApi, IdentityAttributeBeta, PasswordConfigurationApi, PasswordOrgConfig, PasswordManagementBetaApi, ConnectorRuleUpdateRequestBeta, IdentitiesBetaApi, IdentitiesBetaApiListIdentitiesRequest, IdentityBeta, IdentitySyncJobBeta, TaskResultResponseBeta, LoadEntitlementTaskBeta, TaskManagementBetaApi, TaskStatusBeta, EntitlementSourceResetBaseReferenceDtoBeta, TaskResultDtoBeta, ProvisioningPolicyDto, ImportFormDefinitionsRequestInnerBeta, ManagedClustersBetaApi, StandardLevelBeta, CertificationCampaignsApi, CertificationsApi, CertificationCampaignsApiMoveRequest, CertificationSummariesApi, IdentityCertDecisionSummary, AccessReviewItem, CertificationCampaignFiltersApiFp, IdentityCertificationDto, GetActiveCampaigns200ResponseInner, CertificationsApiSubmitReassignCertsAsyncRequest, WorkflowsApi, ExportPayloadBetaIncludeTypesBeta, SODPoliciesV2024Api, SodPolicyV2024, CertificationTask, AppsBetaApi, SourceAppBeta } from 'sailpoint-api-client';
11+
import { Configuration, IdentityProfilesApi, IdentityProfile, LifecycleState, LifecycleStatesApi, Paginator, ServiceDeskIntegrationApi, ServiceDeskIntegrationDto, Source, SourcesApi, TransformsApi, WorkflowsBetaApi, WorkflowBeta, WorkflowExecutionBeta, WorkflowLibraryTriggerBeta, ConnectorRuleManagementBetaApi, ConnectorRuleResponseBeta, ConnectorRuleValidationResponseBeta, AccountsApi, AccountsApiListAccountsRequest, Account, EntitlementsBetaApi, EntitlementsBetaApiListEntitlementsRequest, PublicIdentitiesApi, PublicIdentitiesApiGetPublicIdentitiesRequest, PublicIdentity, JsonPatchOperationBeta, SPConfigBetaApi, SpConfigImportResultsBeta, SpConfigJobBeta, ImportOptionsBeta, SpConfigExportResultsBeta, ObjectExportImportOptionsBeta, TransformRead, GovernanceGroupsBetaApi, WorkgroupDtoBeta, AccessProfilesApi, AccessProfilesApiListAccessProfilesRequest, AccessProfile, RolesApi, Role, RolesApiListRolesRequest, Search, SearchApi, IdentityDocument, SearchDocument, AccessProfileDocument, EntitlementDocument, EntitlementBeta, RoleDocument, SourcesBetaApi, StatusResponseBeta, Schema, FormBeta, CustomFormsBetaApi, ExportFormDefinitionsByTenant200ResponseInnerBeta, FormDefinitionResponseBeta, NotificationsBetaApi, TemplateDtoBeta, SegmentsApi, Segment, SearchAttributeConfigurationBetaApi, SearchAttributeConfigBeta, IdentityAttributesBetaApi, IdentityAttributeBeta, PasswordConfigurationApi, PasswordOrgConfig, PasswordManagementBetaApi, ConnectorRuleUpdateRequestBeta, IdentitiesBetaApi, IdentitiesBetaApiListIdentitiesRequest, IdentityBeta, IdentitySyncJobBeta, TaskResultResponseBeta, LoadEntitlementTaskBeta, TaskManagementBetaApi, TaskStatusBeta, EntitlementSourceResetBaseReferenceDtoBeta, TaskResultDtoBeta, ProvisioningPolicyDto, ImportFormDefinitionsRequestInnerBeta, ManagedClustersBetaApi, StandardLevelBeta, CertificationCampaignsApi, CertificationsApi, CertificationCampaignsApiMoveRequest, CertificationSummariesApi, IdentityCertDecisionSummary, AccessReviewItem, CertificationCampaignFiltersApiFp, IdentityCertificationDto, GetActiveCampaigns200ResponseInner, CertificationsApiSubmitReassignCertsAsyncRequest, WorkflowsApi, ExportPayloadBetaIncludeTypesBeta, SODPoliciesV2024Api, SodPolicyV2024, CertificationTask, AppsBetaApi, SourceAppBeta, ConfigurationHubV2024Api, BackupResponseV2024 } from 'sailpoint-api-client';
1212
import { DEFAULT_PUBLIC_IDENTITIES_QUERY_PARAMS } from '../models/PublicIdentity';
1313
import axios, { AxiosInstance, AxiosResponse } from 'axios';
1414
import { ImportEntitlementsResult } from '../models/JobStatus';
@@ -422,6 +422,52 @@ export class ISCClient {
422422
//#endregion Transforms
423423
////////////////////////
424424

425+
/////////////////////
426+
//#region Configuration Hub
427+
/////////////////////
428+
x
429+
430+
public async uploadBackup(data: string, fileName:string, name: string): Promise<BackupResponseV2024> {
431+
console.log("> uploadBackup");
432+
433+
434+
const fileBuffer = Buffer.from(data)
435+
436+
// Create a File object
437+
const file = new File([fileBuffer], fileName, {
438+
type: "application/json"
439+
})
440+
441+
442+
const apiConfig = await this.getApiConfiguration()
443+
const api = new ConfigurationHubV2024Api(apiConfig, undefined, this.getAxiosWithInterceptors());
444+
445+
const result = await api.createUploadedConfiguration({
446+
data: file,
447+
name
448+
})
449+
return result.data
450+
}
451+
452+
453+
/**
454+
* cf. https://developer.sailpoint.com/docs/api/v2024/get-uploaded-configuration
455+
* @param jobId
456+
* @returns
457+
*/
458+
public async getUploadConfigurationJobStatus(jobId: string): Promise<BackupResponseV2024> {
459+
console.log("> getUploadConfigurationJobStatus", jobId);
460+
const apiConfig = await this.getApiConfiguration();
461+
const api = new ConfigurationHubV2024Api(apiConfig, undefined, this.getAxiosWithInterceptors());
462+
const response = await api.getUploadedConfiguration({id:jobId})
463+
return response.data;
464+
}
465+
466+
467+
////////////////////////
468+
//#endregion Configuration Hub
469+
////////////////////////
470+
425471
/////////////////////////////
426472
//#region Generic methods
427473
/////////////////////////////

src/utils/showInputBox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { Disposable, InputBox, InputBoxOptions, QuickInputButton, QuickInputButtons, Uri, env, window } from 'vscode';
6+
import { Disposable, InputBox, QuickInputButton, QuickInputButtons, Uri, env, window } from 'vscode';
77
import { GoBackError, UserCancelledError } from '../errors';
88
import { Wizard } from '../wizard/wizard';
99
import { LearnMore } from '../wizard/LearnMoreButton';

src/utils/vsCodeHelpers.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,17 +202,30 @@ export async function openPreview(uri: vscode.Uri | string, language = "json", p
202202
* @param extension the extension (e.g. 'json')
203203
*/
204204
export async function chooseFile(fileType: string, extension: string): Promise<undefined | vscode.Uri> {
205-
const fileUri = await vscode.window.showOpenDialog({
205+
const options = {
206206
canSelectMany: false,
207207
openLabel: 'Open',
208208
filters: {
209209
[fileType]: [extension],
210210
// eslint-disable-next-line @typescript-eslint/naming-convention
211211
'All files': ['*']
212212
}
213-
});
213+
}
214+
215+
return await chooseFileExtended(options) as undefined | vscode.Uri
216+
}
217+
218+
219+
export async function chooseFileExtended(options: vscode.OpenDialogOptions): Promise<undefined | vscode.Uri | vscode.Uri[]> {
220+
const fileUri = await vscode.window.showOpenDialog(options);
221+
222+
if (fileUri === undefined || fileUri.length === 0) { return undefined }
223+
if (options.canSelectMany) {
224+
return fileUri
225+
} else {
226+
return fileUri[0];
227+
}
214228

215-
return fileUri === undefined || fileUri.length === 0 ? undefined : fileUri[0];
216229
}
217230

218231

0 commit comments

Comments
 (0)