From 8ee38936be73e573c994d20ff31aceeaebe9f920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannick=20B=C3=A9ot?= Date: Tue, 20 Dec 2022 22:01:06 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=93=9A=F0=9F=A7=B9cleanup=20and=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/importConfig.ts | 44 ++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/commands/importConfig.ts b/src/commands/importConfig.ts index 01797a6..0b8a26e 100644 --- a/src/commands/importConfig.ts +++ b/src/commands/importConfig.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; // import path = require('path'); import { TenantService } from '../services/TenantService'; import { chooseTenant, getFullContent } from '../utils/vsCodeHelpers'; -import { ExportOptions, ObjectOptions } from '../models/ExportOptions'; +import { ExportOptions } from '../models/ExportOptions'; import { ObjectPickItem } from '../models/ObjectPickItem'; import { OBJECT_TYPE_ITEMS } from '../models/ObjectTypeQuickPickItem'; import { ImportedObject } from '../models/JobStatus'; @@ -22,14 +22,19 @@ const PICK_AND_CHOOSE: vscode.QuickPickItem = { }; - -class Importer { +/** + * Base class for all importer + */ +class BaseImporter { tenantName: string | undefined; tenantId: string | undefined; filePath: string | undefined; data = ""; importOptions: ExportOptions = {}; + /** + * Create the import job and follow-up the result + */ async importConfig(): Promise { if (!this.tenantId || !this.tenantName) { throw new Error("Invalid tenant info"); @@ -48,8 +53,6 @@ class Importer { cancellable: false }, async (task, token) => { try { - - const jobId = await client.startImportJob(data, importOptions); let jobStatus: any; do { @@ -70,22 +73,21 @@ class Importer { message += importJobresult.results[key].importedObjects .map((x: ImportedObject) => `${x.name} (${key})`) .join(", "); - } } catch (error: any) { vscode.window.showErrorMessage(`Could not import data: ${error.message}`); throw error; - } - - }).then(() => { - vscode.window.showInformationMessage( + }).then(async () => { + await vscode.window.showInformationMessage( `Successfully imported configuration to ${this.tenantName}: ${message}`) }); } - - + /** + * Asks the user if he/she wants to import everything or not + * @returns ALL or PICK_AND_CHOOSE + */ async askImportAll(): Promise { const result = await vscode.window.showQuickPick( [ALL, PICK_AND_CHOOSE], @@ -101,7 +103,11 @@ class Importer { } }; - + /** + * Asks the user to choose from a list of ObjectPickItem + * @param items List of ObjectPickItem + * @returns List of ids + */ async askChosenItems(items: Array): Promise | undefined> { const result = await vscode.window.showQuickPick( items, @@ -117,7 +123,11 @@ class Importer { } }; - + /** + * Maps object types to QuickPickItems with a human-readable label and asks to choose + * @param objectTypes List of object types to choose from + * @returns + */ async askSelectObjectTypes(objectTypes: Set): Promise | undefined> { const sortedObjectTypeItems = OBJECT_TYPE_ITEMS .filter(x => objectTypes.has(x.objectType)) @@ -190,7 +200,7 @@ class Importer { * Entry point to import file from the command palette. Tenant is unknown. File is known. * @param node */ -export class PaletteImporter extends Importer { +export class PaletteImporter extends BaseImporter { constructor( private readonly tenantService: TenantService ) { super(); } @@ -228,7 +238,7 @@ export class PaletteImporter extends Importer { * Entry point to import file from the explorer. Tenant is unknown. File is known. * @param node */ -export class MenuImporter extends Importer { +export class MenuImporter extends BaseImporter { constructor( private readonly tenantService: TenantService ) { super(); } @@ -257,7 +267,7 @@ export class MenuImporter extends Importer { * Entry point to import file from the tree view. Tenant is already known * @param node */ -export class TreeViewImporter extends Importer { +export class TreeViewImporter extends BaseImporter { constructor( private readonly tenantService: TenantService ) { super(); } From 119f1f7d50d056870bcb7defd86b2a1652978a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannick=20B=C3=A9ot?= Date: Tue, 20 Dec 2022 22:36:26 +0100 Subject: [PATCH 2/2] #30 Can refresh identities under an identity profile --- CHANGELOG.md | 1 + README.md | 2 +- package.json | 18 ++++++++++++----- src/commands/constants.ts | 1 + src/commands/importConfig.ts | 2 +- src/commands/refreshIdentityProfile.ts | 28 ++++++++++++++++++++++++++ src/extension.ts | 5 +++++ src/services/IdentityNowClient.ts | 19 +++++++++++++++-- 8 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 src/commands/refreshIdentityProfile.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 799a590..e1b7e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This changelog is following the recommended format by [keepachangelog](https://k - Capability to export a single source, rule, transform or identity profile from the tree view - Capability to import a sp-config +- Can refresh identities under an identity profile (cf. [#30](https://github.com/yannick-beot-sp/vscode-sailpoint-identitynow/issues/30)) ### Fixed diff --git a/README.md b/README.md index ae32997..8b0c909 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The SailPoint IdentityNow extension makes it easy to: - View, edit, enable, disable, and test workflows and view execution history - View, create, edit, delete connector rules and export/import the script of a rule - View, edit, delete service desk integrations -- View, edit, delete identity profiles and lifecycle states +- View, edit, delete identity profiles and lifecycle states, and refreshes all the identities under a profile ## Installation diff --git a/package.json b/package.json index f2055e2..b6604b0 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "onCommand:vscode-sailpoint-identitynow.connector-rule.export-script.view", "onCommand:vscode-sailpoint-identitynow.identity-profiles.sort.name", "onCommand:vscode-sailpoint-identitynow.identity-profiles.sort.priority", + "onCommand:vscode-sailpoint-identitynow.identity-profile.refresh", "onFileSystem:idn", "onUri" ], @@ -239,6 +240,10 @@ "command": "vscode-sailpoint-identitynow.connector-rule.export-script.view", "title": "Export the script..." }, + { + "command": "vscode-sailpoint-identitynow.identity-profile.refresh", + "title": "Refresh all identities" + }, { "command": "vscode-sailpoint-identitynow.identity-profiles.sort.name", "title": "Sort by name", @@ -334,6 +339,10 @@ "command": "vscode-sailpoint-identitynow.transform.evaluate", "when": "never" }, + { + "command": "vscode-sailpoint-identitynow.identity-profile.refresh", + "when": "never" + }, { "command": "vscode-sailpoint-identitynow.identity-profiles.sort.name", "when": "never" @@ -506,6 +515,10 @@ "command": "vscode-sailpoint-identitynow.connector-rule.export-script.view", "when": "view == vscode-sailpoint-identitynow.view && viewItem == connector-rule" }, + { + "command": "vscode-sailpoint-identitynow.identity-profile.refresh", + "when": "view == vscode-sailpoint-identitynow.view && viewItem == identity-profile" + }, { "command": "vscode-sailpoint-identitynow.refresh", "when": "view == vscode-sailpoint-identitynow.view && viewItem == identity-profiles", @@ -521,11 +534,6 @@ "when": "view == vscode-sailpoint-identitynow.view && viewItem == identity-profiles", "group": "inline@1" }, - { - "command": "vscode-sailpoint-identitynow.refresh", - "when": "view == vscode-sailpoint-identitynow.view && viewItem == identity-profile", - "group": "inline@1" - }, { "command": "vscode-sailpoint-identitynow.open-resource", "when": "view == vscode-sailpoint-identitynow.view && viewItem == identity-profile" diff --git a/src/commands/constants.ts b/src/commands/constants.ts index 6638a85..2d52f21 100644 --- a/src/commands/constants.ts +++ b/src/commands/constants.ts @@ -31,6 +31,7 @@ export const EXPORT_CONNECTOR_RULE_SCRIPT_EDITOR = 'vscode-sailpoint-identitynow export const EXPORT_CONNECTOR_RULE_SCRIPT_VIEW = 'vscode-sailpoint-identitynow.connector-rule.export-script.view'; export const SORT_IDENTITY_PROFILES_BY_NAME = 'vscode-sailpoint-identitynow.identity-profiles.sort.name'; export const SORT_IDENTITY_PROFILES_BY_PRIORITY = 'vscode-sailpoint-identitynow.identity-profiles.sort.priority'; +export const REFRESH_IDENTITY_PROFILE = 'vscode-sailpoint-identitynow.identity-profile.refresh'; export const TREE_VIEW = 'vscode-sailpoint-identitynow.view'; export const WORKFLOW_TESTER_VIEW = 'vscode-sailpoint-identitynow.workflow.test-view'; \ No newline at end of file diff --git a/src/commands/importConfig.ts b/src/commands/importConfig.ts index 0b8a26e..3c8d635 100644 --- a/src/commands/importConfig.ts +++ b/src/commands/importConfig.ts @@ -65,7 +65,7 @@ class BaseImporter { throw new Error("Could not import config: " + jobStatus.message); } - const importJobresult = await client.getImportJobResult(jobId); + const importJobresult:any = await client.getImportJobResult(jobId); for (const key in importJobresult.results) { if (message.length > 0) { message += ", "; diff --git a/src/commands/refreshIdentityProfile.ts b/src/commands/refreshIdentityProfile.ts new file mode 100644 index 0000000..0bab2c8 --- /dev/null +++ b/src/commands/refreshIdentityProfile.ts @@ -0,0 +1,28 @@ +import * as vscode from 'vscode'; +import * as commands from './constants'; +import { IdentityNowResourceTreeItem, IdentityProfileTreeItem, TransformsTreeItem } from '../models/IdentityNowTreeItem'; +import { IdentityNowClient } from '../services/IdentityNowClient'; +import { getPathByUri } from '../utils/UriUtils'; + + +export async function refreshIdentityProfile(node?: IdentityProfileTreeItem): Promise { + + console.log("> refreshIdentityProfile", node); + // assessing that item is a IdentityProfileTreeItem + if (node === undefined || !(node instanceof IdentityProfileTreeItem)) { + console.log("WARNING: refreshIdentityProfile: invalid item", node); + throw new Error("refreshIdentityProfile: invalid item"); + } + + + const client = new IdentityNowClient(node.tenantId, node.tenantName); + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: `Refreshing ${node.label}...`, + cancellable: false + }, async (task, token) => { + await client.refreshIdentityProfile(node.id); + }).then(async () => + await vscode.window.showInformationMessage(`Successfully refreshed ${node.label}`)); + +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 916793f..eb29a80 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,6 +27,7 @@ import { RenameTenantCommand } from './commands/renameTenant'; import { IdentityNowUriHandler } from './uriHandler'; import { SortIdentityProfileCommand } from './commands/sortIdentityProfile'; import { MenuImporter, PaletteImporter, TreeViewImporter } from './commands/importConfig'; +import { refreshIdentityProfile } from './commands/refreshIdentityProfile'; // this method is called when your extension is activated // your extension is activated the very first time the command is executed @@ -188,6 +189,10 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand(commands.EXPORT_CONNECTOR_RULE_SCRIPT_VIEW, exportScriptFromRuleCommand.exportScriptView, exportScriptFromRuleCommand)); + context.subscriptions.push( + vscode.commands.registerCommand(commands.REFRESH_IDENTITY_PROFILE, + refreshIdentityProfile)); + const sortIdentityProfileCommand = new SortIdentityProfileCommand(); context.subscriptions.push( vscode.commands.registerCommand(commands.SORT_IDENTITY_PROFILES_BY_NAME, diff --git a/src/services/IdentityNowClient.ts b/src/services/IdentityNowClient.ts index fd769da..38df8ce 100644 --- a/src/services/IdentityNowClient.ts +++ b/src/services/IdentityNowClient.ts @@ -15,6 +15,7 @@ import { Readable } from "stream"; import { ImportJobResults, JobStatus } from "../models/JobStatus"; export class IdentityNowClient { + constructor( private readonly tenantId: string, private readonly tenantName: string @@ -496,8 +497,7 @@ export class IdentityNowClient { objectOptions = {} ): Promise { console.log("> startExportJob", objectTypes); - let endpoint = EndpointUtils.getBetaUrl(this.tenantName); - endpoint += "/sp-config/export"; + const endpoint = EndpointUtils.getBetaUrl(this.tenantName) + "/sp-config/export"; console.log("endpoint = " + endpoint); const headers = await this.prepareHeaders(); @@ -796,6 +796,21 @@ export class IdentityNowClient { // identityProfiles.sort(compareByName); return serviceDesks; } + + public async refreshIdentityProfile(identityProfileId: string):Promise { + console.log("> refreshIdentityProfile", identityProfileId); + const endpoint = EndpointUtils.getBetaUrl(this.tenantName) + `/identity-profiles/${identityProfileId}/refresh-identities`; + console.log("endpoint = " + endpoint); + const headers = await this.prepareHeaders(); + const resp = await fetch(endpoint, { + method: "POST", + headers: headers + }); + + if (!resp.ok) { + throw new Error(resp.statusText); + } + } } export enum AggregationJob {