@robertraaijmakers/pptb-securityplugin 0.1.0 → 0.1.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,7 @@
1
1
  # Security Roles Explorer
2
-
3
- Power Platform ToolBox tool for inspecting and managing Dataverse security roles, table privileges, and user assignments.
4
-
5
- ## Overview
6
-
7
- Security Roles Explorer helps you quickly understand which roles grant access to which tables and lets you update privileges or user-role assignments in one place.
2
+ Power Platform ToolBox tool for inspecting and managing Dataverse security roles, table privileges, and user assignments. Helps you quickly understand which roles grant access to which tables and lets you update privileges or user-role assignments in one place.
8
3
 
9
4
  ## Features
10
-
11
5
  - View role privileges by role or by table
12
6
  - Batch cache of role privilege data for faster loading
13
7
  - Sort and filter by privilege level
@@ -19,12 +13,10 @@ Security Roles Explorer helps you quickly understand which roles grant access to
19
13
  - Light/dark theme support with a manual toggle
20
14
 
21
15
  ## Requirements
22
-
23
16
  - Power Platform ToolBox (desktop app)
24
17
  - Node.js 18+ (recommended)
25
18
 
26
19
  ## Development
27
-
28
20
  Install dependencies:
29
21
 
30
22
  ```bash
@@ -43,24 +35,18 @@ Watch mode:
43
35
  npm run build:watch
44
36
  ```
45
37
 
46
- ## Loading in ToolBox
47
-
38
+ ## Loading in ToolBox (for developers)
48
39
  1. Enable Debug Menu in ToolBox settings.
49
40
  2. Use Debug > Load Local Tool and select this folder.
50
41
  3. Close and reopen the tool to refresh changes.
51
42
 
52
43
  ## Usage
53
-
54
44
  1. Select a connection in ToolBox.
55
- 2. Choose a filter mode:
56
- - By role: review privileges for a single role.
57
- - By entity: compare multiple roles for a single table.
58
- 3. Use the filters and sort controls to narrow results.
59
- 4. Change privilege levels in the grid and click Apply changes.
60
- 5. Use the Assign security roles tab to add/remove roles for users.
45
+ 2. Use the filters and sort controls to narrow results.
46
+ 3. Change privilege levels in the grid and click Apply changes.
47
+ 4. Use the Assign security roles tab to add/remove roles for users.
61
48
 
62
49
  ## Contributing
63
-
64
50
  1. Fork the repo.
65
51
  2. Create a feature branch.
66
52
  3. Make changes with small, focused commits.
@@ -68,11 +54,9 @@ npm run build:watch
68
54
  5. Open a pull request with a clear description and screenshots if UI changes.
69
55
 
70
56
  ## Troubleshooting
71
-
72
57
  - If the tool shows "Not connected", select a connection in ToolBox and reload.
73
58
  - If role privileges fail to load, verify the connection user has the required security role permissions.
74
59
  - If UI changes do not appear, ensure `npm run build` completed and reopen the tool.
75
60
 
76
61
  ## License
77
-
78
- MIT
62
+ MIT
package/dist/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Security Roles Explorer
2
+ Power Platform ToolBox tool for inspecting and managing Dataverse security roles, table privileges, and user assignments. Helps you quickly understand which roles grant access to which tables and lets you update privileges or user-role assignments in one place.
3
+
4
+ ## Features
5
+ - View role privileges by role or by table
6
+ - Batch cache of role privilege data for faster loading
7
+ - Sort and filter by privilege level
8
+ - Rights filter (all / with rights / without rights)
9
+ - Role multi-select filter (entity mode)
10
+ - Apply or undo pending privilege changes
11
+ - Assign or remove roles for users (role -> users or user -> roles)
12
+ - Activity log with timestamps
13
+ - Light/dark theme support with a manual toggle
14
+
15
+ ## Requirements
16
+ - Power Platform ToolBox (desktop app)
17
+ - Node.js 18+ (recommended)
18
+
19
+ ## Development
20
+ Install dependencies:
21
+
22
+ ```bash
23
+ npm install
24
+ ```
25
+
26
+ Build once:
27
+
28
+ ```bash
29
+ npm run build
30
+ ```
31
+
32
+ Watch mode:
33
+
34
+ ```bash
35
+ npm run build:watch
36
+ ```
37
+
38
+ ## Loading in ToolBox (for developers)
39
+ 1. Enable Debug Menu in ToolBox settings.
40
+ 2. Use Debug > Load Local Tool and select this folder.
41
+ 3. Close and reopen the tool to refresh changes.
42
+
43
+ ## Usage
44
+ 1. Select a connection in ToolBox.
45
+ 2. Use the filters and sort controls to narrow results.
46
+ 3. Change privilege levels in the grid and click Apply changes.
47
+ 4. Use the Assign security roles tab to add/remove roles for users.
48
+
49
+ ## Contributing
50
+ 1. Fork the repo.
51
+ 2. Create a feature branch.
52
+ 3. Make changes with small, focused commits.
53
+ 4. Run `npm run build` and verify in ToolBox.
54
+ 5. Open a pull request with a clear description and screenshots if UI changes.
55
+
56
+ ## Troubleshooting
57
+ - If the tool shows "Not connected", select a connection in ToolBox and reload.
58
+ - If role privileges fail to load, verify the connection user has the required security role permissions.
59
+ - If UI changes do not appear, ensure `npm run build` completed and reopen the tool.
60
+
61
+ ## License
62
+ MIT
package/dist/app.js CHANGED
@@ -14545,6 +14545,14 @@ ${logTarget.textContent}`;
14545
14545
 
14546
14546
  // src/services/dataverseService.ts
14547
14547
  var dataverseAPI = window.dataverseAPI;
14548
+ function buildPrivilegeReference(privilegeId) {
14549
+ const apiEndpoint = dataverseAPI?.apiEndpoint;
14550
+ if (!apiEndpoint) {
14551
+ return `privileges(${privilegeId})`;
14552
+ }
14553
+ const separator = apiEndpoint.endsWith("/") ? "" : "/";
14554
+ return `${apiEndpoint}${separator}privileges(${privilegeId})`;
14555
+ }
14548
14556
  async function queryAll(odataQuery) {
14549
14557
  const all = [];
14550
14558
  let response = await dataverseAPI.queryData(odataQuery);
@@ -14675,6 +14683,35 @@ ${logTarget.textContent}`;
14675
14683
  const response = await dataverseAPI.queryData(query);
14676
14684
  return response?.RolePrivileges ?? response?.rolePrivileges ?? response?.value ?? [];
14677
14685
  }
14686
+ async function addPrivilegesToRole(roleId, privileges) {
14687
+ if (privileges.length === 0) {
14688
+ return;
14689
+ }
14690
+ await dataverseAPI.execute({
14691
+ entityName: "role",
14692
+ entityId: roleId,
14693
+ operationName: "AddPrivilegesRole",
14694
+ operationType: "action",
14695
+ parameters: {
14696
+ Privileges: privileges
14697
+ }
14698
+ });
14699
+ }
14700
+ async function removePrivilegesFromRole(roleId, privilegeId) {
14701
+ if (!privilegeId) {
14702
+ return;
14703
+ }
14704
+ const privilegeReference = buildPrivilegeReference(privilegeId);
14705
+ await dataverseAPI.execute({
14706
+ entityName: "role",
14707
+ entityId: roleId,
14708
+ operationName: "RemovePrivilegeRole",
14709
+ operationType: "action",
14710
+ parameters: {
14711
+ Privilege: privilegeReference
14712
+ }
14713
+ });
14714
+ }
14678
14715
  async function associateRoleToUser(userId, roleId) {
14679
14716
  await dataverseAPI.associate(
14680
14717
  "systemuser",
@@ -14741,6 +14778,7 @@ ${logTarget.textContent}`;
14741
14778
  loadingRolesMetadata: "Loading roles and metadata",
14742
14779
  loadingRolePrivileges: "Loading role privileges",
14743
14780
  loadingRefreshingPrivileges: "Refreshing role privileges",
14781
+ loadingApplyingChanges: "Applying privilege changes",
14744
14782
  loadingDashboardData: "Loading dashboard data",
14745
14783
  loadingDashboardRolesTeams: "Loading roles and teams",
14746
14784
  loadingDashboardPeople: "Loading users, business units, and team memberships",
@@ -14791,10 +14829,6 @@ ${logTarget.textContent}`;
14791
14829
  title: "Update failed",
14792
14830
  body: "Failed to apply privilege updates. See console for details."
14793
14831
  },
14794
- updated: {
14795
- title: "Privileges updated",
14796
- body: "Your changes have been queued for update."
14797
- },
14798
14832
  cacheFailed: {
14799
14833
  title: "Cache failed",
14800
14834
  body: "Could not cache role privileges. See console for details."
@@ -14846,9 +14880,6 @@ ${logTarget.textContent}`;
14846
14880
  function formatMissingPrivilegeId(entityLogicalName, privilege) {
14847
14881
  return `Missing privilege ID for ${entityLogicalName}:${privilege}`;
14848
14882
  }
14849
- function formatQueuedPrivilegeChange(privilege, entityLogicalName, level) {
14850
- return `Queued ${privilege} change for ${entityLogicalName}: ${level}`;
14851
- }
14852
14883
  function formatNoPrivilegesForRole(roleName) {
14853
14884
  return `No privileges returned for role ${roleName}.`;
14854
14885
  }
@@ -14944,6 +14975,7 @@ ${logTarget.textContent}`;
14944
14975
  direction: "asc"
14945
14976
  },
14946
14977
  assignmentFilter: "",
14978
+ assignmentSearch: "",
14947
14979
  sort: {
14948
14980
  column: "label",
14949
14981
  direction: "asc"
@@ -15004,6 +15036,7 @@ ${logTarget.textContent}`;
15004
15036
  document.querySelectorAll("[data-assign-sort]")
15005
15037
  ),
15006
15038
  assignmentFilterAssigned: document.getElementById("assignment-filter-assigned"),
15039
+ assignmentSearch: document.getElementById("assignment-search"),
15007
15040
  controlsPrivileges: document.getElementById("controls-privileges"),
15008
15041
  controlsAssignments: document.getElementById("controls-assignments"),
15009
15042
  controlsDashboard: document.getElementById("controls-dashboard"),
@@ -15424,6 +15457,17 @@ ${logTarget.textContent}`;
15424
15457
  if (state.assignmentFilter === "not-assigned") {
15425
15458
  return !item.assigned;
15426
15459
  }
15460
+ if (state.assignmentSearch) {
15461
+ const rawTerm = state.assignmentSearch.toLowerCase();
15462
+ const term = rawTerm.replace(/\*/g, "").trim();
15463
+ if (term) {
15464
+ const labelMatch = item.label.toLowerCase().includes(term);
15465
+ const subLabelMatch = item.subLabel ? item.subLabel.toLowerCase().includes(term) : false;
15466
+ if (!labelMatch && !subLabelMatch) {
15467
+ return false;
15468
+ }
15469
+ }
15470
+ }
15427
15471
  return true;
15428
15472
  });
15429
15473
  return [...filtered].sort((a, b) => {
@@ -15768,7 +15812,6 @@ ${logTarget.textContent}`;
15768
15812
  const level = select.value;
15769
15813
  const isPending = updatePendingChange(roleId, row.entityLogicalName, privilege, level);
15770
15814
  setPendingClass(select, isPending);
15771
- logMessage(formatQueuedPrivilegeChange(privilege, row.entityLogicalName, level));
15772
15815
  });
15773
15816
  select.disabled = !isRoleMode && !row.roleId;
15774
15817
  applyLevelClass(select, select.value);
@@ -15929,18 +15972,18 @@ ${logTarget.textContent}`;
15929
15972
  }
15930
15973
  return "none";
15931
15974
  }
15932
- function mapPrivilegeDepth(level) {
15975
+ function mapPrivilegeDepthLabel(level) {
15933
15976
  switch (level) {
15934
15977
  case "user":
15935
- return 1;
15978
+ return "Basic";
15936
15979
  case "businessUnit":
15937
- return 2;
15980
+ return "Local";
15938
15981
  case "parentChild":
15939
- return 4;
15982
+ return "Deep";
15940
15983
  case "organization":
15941
- return 8;
15984
+ return "Global";
15942
15985
  default:
15943
- return 0;
15986
+ return "None";
15944
15987
  }
15945
15988
  }
15946
15989
  function mapOwnershipLabel(raw) {
@@ -17266,42 +17309,44 @@ ${logTarget.textContent}`;
17266
17309
  if (currentLevel === change.level) {
17267
17310
  continue;
17268
17311
  }
17269
- if (currentLevel !== "none") {
17312
+ if (change.level === "none") {
17270
17313
  if (!removesByRole.has(change.roleId)) {
17271
17314
  removesByRole.set(change.roleId, []);
17272
17315
  }
17273
17316
  removesByRole.get(change.roleId).push(privilegeId);
17274
- }
17275
- if (change.level !== "none") {
17317
+ } else {
17276
17318
  if (!addsByRole.has(change.roleId)) {
17277
17319
  addsByRole.set(change.roleId, []);
17278
17320
  }
17321
+ const privilegeInfo = state.privilegeInfoById.get(privilegeId);
17279
17322
  addsByRole.get(change.roleId).push({
17280
17323
  PrivilegeId: privilegeId,
17281
- Depth: mapPrivilegeDepth(change.level)
17324
+ Depth: mapPrivilegeDepthLabel(change.level),
17325
+ PrivilegeName: privilegeInfo?.name
17282
17326
  });
17283
17327
  }
17284
17328
  }
17329
+ const totalRemoveCalls = Array.from(removesByRole.values()).reduce(
17330
+ (total, privilegeIds) => total + privilegeIds.length,
17331
+ 0
17332
+ );
17333
+ const totalAddCalls = addsByRole.size;
17334
+ const totalCalls = totalRemoveCalls + totalAddCalls;
17335
+ let completedCalls = 0;
17336
+ setLoading(true, UI_TEXT.loadingApplyingChanges);
17337
+ updateLoadingProgress(0, totalCalls, UI_TEXT.loadingApplyingChanges);
17285
17338
  try {
17286
17339
  for (const [roleId, privilegeIds] of removesByRole) {
17287
- await dataverseAPI.execute({
17288
- operationName: "RemovePrivilegesRole",
17289
- operationType: "action",
17290
- parameters: {
17291
- RoleId: roleId,
17292
- PrivilegeIds: privilegeIds
17293
- }
17294
- });
17340
+ for (const privilegeId of privilegeIds) {
17341
+ await removePrivilegesFromRole(roleId, privilegeId);
17342
+ completedCalls += 1;
17343
+ updateLoadingProgress(completedCalls, totalCalls, UI_TEXT.loadingApplyingChanges);
17344
+ }
17295
17345
  }
17296
17346
  for (const [roleId, privileges] of addsByRole) {
17297
- await dataverseAPI.execute({
17298
- operationName: "AddPrivilegesRole",
17299
- operationType: "action",
17300
- parameters: {
17301
- RoleId: roleId,
17302
- Privileges: privileges
17303
- }
17304
- });
17347
+ await addPrivilegesToRole(roleId, privileges);
17348
+ completedCalls += 1;
17349
+ updateLoadingProgress(completedCalls, totalCalls, UI_TEXT.loadingApplyingChanges);
17305
17350
  }
17306
17351
  for (const change of state.pendingChanges) {
17307
17352
  if (!state.rolePrivileges.has(change.roleId)) {
@@ -17320,16 +17365,12 @@ ${logTarget.textContent}`;
17320
17365
  duration: 3500
17321
17366
  });
17322
17367
  return;
17368
+ } finally {
17369
+ setLoading(false);
17323
17370
  }
17324
17371
  state.pendingChanges = [];
17325
- await toolboxAPI2.utils.showNotification({
17326
- title: NOTIFICATIONS.updated.title,
17327
- body: NOTIFICATIONS.updated.body,
17328
- type: "success",
17329
- duration: 2500
17330
- });
17331
17372
  updatePendingUi();
17332
- renderPrivilegeTable();
17373
+ await refreshPrivilegeView();
17333
17374
  }
17334
17375
  async function refreshData() {
17335
17376
  if (state.refreshInProgress) {
@@ -17618,6 +17659,12 @@ ${logTarget.textContent}`;
17618
17659
  renderAssignmentTable(state.assignmentItems);
17619
17660
  });
17620
17661
  }
17662
+ if (elements2.assignmentSearch) {
17663
+ elements2.assignmentSearch.addEventListener("input", () => {
17664
+ state.assignmentSearch = elements2.assignmentSearch.value.trim();
17665
+ renderAssignmentTable(state.assignmentItems);
17666
+ });
17667
+ }
17621
17668
  for (const button of elements2.sortButtons) {
17622
17669
  button.addEventListener("click", () => {
17623
17670
  const column = button.dataset.sort || "label";