@mindline/sync 1.0.56 → 1.0.58

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.
@@ -2,6 +2,5 @@
2
2
  "ExpandedNodes": [
3
3
  ""
4
4
  ],
5
- "SelectedNode": "\\index.ts",
6
5
  "PreviewInSolutionExplorer": false
7
6
  }
package/.vs/slnx.sqlite CHANGED
Binary file
Binary file
package/index.d.ts CHANGED
@@ -30,6 +30,8 @@ declare module "@mindline/sync" {
30
30
  static graphGroupsEndpoint: string;
31
31
  static graphMailEndpoint: string;
32
32
  static graphMeEndpoint: string;
33
+ static graphOauth2PermissionGrants: string;
34
+ static graphServicePrincipalsEndpoint: string;
33
35
  static graphUsersEndpoint: string;
34
36
  // sovereign cloud tenant info endpoints
35
37
  static graphTenantByDomainPredicate: string;
@@ -52,6 +54,7 @@ declare module "@mindline/sync" {
52
54
  group: string;
53
55
  value: string;
54
56
  consented: boolean;
57
+ removable: boolean;
55
58
  expanded: string;
56
59
  static compareByValue(a: UserScope, b: UserScope): number;
57
60
  static compareByGroup(a: UserScope, b: UserScope): number;
@@ -64,7 +67,6 @@ declare module "@mindline/sync" {
64
67
  tid: string; // from AAD ID token
65
68
  companyName: string; // findTenantInformationByTenantId TODO: process changes to company name
66
69
  companyDomain: string; // findTenantInformationByTenantId TODO: process changes to company name
67
- associatedWorkspaces: string[];
68
70
  workspaceIDs: string;
69
71
  session: string;
70
72
  spacode: string;
@@ -99,6 +101,7 @@ declare module "@mindline/sync" {
99
101
  authority: string; // from AAD ID auth response
100
102
  workspaceIDs: string;
101
103
  sel: boolean; // selection state
104
+ graphSP: string; // graph resource ID (service principal) for this tenant
102
105
  constructor();
103
106
  }
104
107
  // config
@@ -223,6 +226,7 @@ declare module "@mindline/sync" {
223
226
  pb_increment: number;
224
227
  pb_idle: number;
225
228
  pb_idleMax: number;
229
+ pb_total: number;
226
230
  pb_timer: NodeJS.Timer;
227
231
  milestoneArray: MilestoneArray;
228
232
  constructor(config: Config|null, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean);
@@ -262,14 +266,18 @@ declare module "@mindline/sync" {
262
266
  //
263
267
  // Azure AD Graph API
264
268
  //
265
- export function groupsGet(instance: IPublicClientApplication, user: User | undefined, groupSearchString: string): Promise<{groups: Group[], error: string}>;
269
+ export function groupsGet(instance: IPublicClientApplication, user: User | undefined, groupSearchString: string): Promise<{ groups: Group[], error: string }>;
270
+ export function oauth2PermissionGrantsGet(options: RequestInit, spid: string, oid: string): Promise<{grants: string, error: string}>;
271
+ export function requestAdminConsent(user: User, scope: string): void;
272
+ export function servicePrincipalGet(options: RequestInit, appid: string): Promise<{ spid: string, error: string }>;
266
273
  export function signIn(user: User, tasks: TaskArray): boolean;
267
274
  export function signInIncrementally(user: User, scope: string): void;
268
- export function requestAdminConsent(user: User, scope: string): void;
269
275
  export function signOut(user: User): boolean;
270
276
  export function tenantRelationshipsGetByDomain(loggedInuser: User, tenant: Tenant, instance: IPublicClientApplication, debug: boolean): boolean;
271
277
  export function tenantRelationshipsGetById(user: User, ii: InitInfo, instance: IPublicClientApplication, tasks: TaskArray, debug: boolean): boolean;
272
- export function tenantUnauthenticatedLookup(tenant: Tenant, debug: boolean): Promise<boolean>;
278
+ export function tenantUnauthenticatedLookup(tenant: Tenant, debug: boolean): boolean;
279
+ export function userDelegatedScopesGet(instance: IPublicClientApplication, loggedInUser: User, tenant: Tenant): { scopes: string, id: string, error: string };
280
+ export function userDelegatedScopesRemove(instance: IPublicClientApplication, loggedInUser: User, tenant: Tenant, scope: string): boolean;
273
281
  export function usersGet(instance: IPublicClientApplication, user: User | undefined): { users: string[], error: string };
274
282
  //
275
283
  // Mindline Config API
package/index.ts CHANGED
@@ -69,6 +69,8 @@ export class graphConfig {
69
69
  static graphGroupsEndpoint: string = "https://graph.microsoft.com/v1.0/groups";
70
70
  static graphMailEndpoint: string = "https://graph.microsoft.com/v1.0/me/messages";
71
71
  static graphMeEndpoint: string = "https://graph.microsoft.com/v1.0/me";
72
+ static graphOauth2PermissionGrants: string = "https://graph.microsoft.com/v1.0/oauth2PermissionGrants";
73
+ static graphServicePrincipalsEndpoint: string = "https://graph.microsoft.com/v1.0/servicePrincipals";
72
74
  static graphUsersEndpoint: string = "https://graph.microsoft.com/v1.0/users";
73
75
  // sovereign cloud tenant info endpoints
74
76
  static graphTenantByDomainPredicate: string = "beta/tenantRelationships/findTenantInformationByDomainName";
@@ -90,6 +92,7 @@ export class UserScope {
90
92
  group: string;
91
93
  value: string;
92
94
  consented: boolean;
95
+ removable: boolean;
93
96
  expanded: string;
94
97
  static compareByValue(a: UserScope, b: UserScope): number {
95
98
  return a.value.localeCompare(b.value);
@@ -106,7 +109,6 @@ export class User {
106
109
  tid: string;
107
110
  companyName: string;
108
111
  companyDomain: string;
109
- associatedWorkspaces: string[];
110
112
  workspaceIDs: string;
111
113
  session: string; // button text
112
114
  spacode: string; // to get front end access token
@@ -119,11 +121,10 @@ export class User {
119
121
  this.oid = "";
120
122
  this.name = "";
121
123
  this.mail = "";
122
- this.authority = "";
124
+ this.authority = "https://login.microsoftonline.com/";
123
125
  this.tid = "";
124
126
  this.companyName = "";
125
127
  this.companyDomain = "";
126
- this.associatedWorkspaces = new Array();
127
128
  this.workspaceIDs = "";
128
129
  this.session = "Sign In";
129
130
  this.spacode = "";
@@ -157,6 +158,7 @@ export class Tenant {
157
158
  authority: string;
158
159
  workspaceIDs: string;
159
160
  sel: boolean; // selection state
161
+ graphSP: string; // graph resource ID (service principal) for this tenant
160
162
  constructor() {
161
163
  this.tid = "";
162
164
  this.name = "";
@@ -167,6 +169,7 @@ export class Tenant {
167
169
  this.authority = "";
168
170
  this.workspaceIDs = "";
169
171
  this.sel = false;
172
+ this.graphSP = "";
170
173
  }
171
174
  }
172
175
  function getGraphEndpoint(authority: string): string {
@@ -381,7 +384,6 @@ export class InitInfo {
381
384
  newuser.tid = user.tid;
382
385
  newuser.companyName = user.companyName;
383
386
  newuser.companyDomain = user.companyDomain;
384
- newuser.associatedWorkspaces = user.associatedWorkspaces;
385
387
  newuser.workspaceIDs = user.workspaceIDs;
386
388
  newuser.session = user.session;
387
389
  newuser.spacode = user.spacode;
@@ -774,6 +776,7 @@ export class BatchArray {
774
776
  pb_increment: number;
775
777
  pb_idle: number;
776
778
  pb_idleMax: number;
779
+ pb_total: number;
777
780
  pb_timer: NodeJS.Timer;
778
781
  milestoneArray: MilestoneArray;
779
782
  constructor(
@@ -789,6 +792,7 @@ export class BatchArray {
789
792
  this.pb_timer = null;
790
793
  this.pb_idle = 0;
791
794
  this.pb_idleMax = 0;
795
+ this.pb_total = 0;
792
796
  this.milestoneArray = new MilestoneArray(false);
793
797
  }
794
798
  // populate tenantNodes based on config tenants
@@ -878,7 +882,7 @@ export class BatchArray {
878
882
  this.pb_increment = .25;
879
883
  this.pb_idle = 0;
880
884
  this.pb_idleMax = 0;
881
- setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}/60]`);
885
+ this.pb_total = 0;
882
886
  this.pb_timer = setInterval(() => {
883
887
  // if signalR has finished the sync, stop the timer
884
888
  if (this.milestoneArray.milestones[0].Write != null) {
@@ -890,15 +894,14 @@ export class BatchArray {
890
894
  }
891
895
  else {
892
896
  // if we've gone 60 seconds without a signalR message, finish the sync
897
+ this.pb_total = this.pb_total + 1;
893
898
  this.pb_idle = this.pb_idle + 1;
894
899
  this.pb_idleMax = Math.max(this.pb_idle, this.pb_idleMax);
895
- setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}/60]`);
900
+ setIdleText(`${this.pb_total} seconds elapsed. Last update ${this.pb_idle} seconds ago. [max idle: ${this.pb_idleMax}/60]`);
896
901
  if (this.pb_idle >= 60) {
897
- clearInterval(this.pb_timer);
898
- this.pb_timer = null;
899
902
  if (this.milestoneArray.milestones[0].Write == null) {
900
903
  this.milestoneArray.write(setMilestones);
901
- setConfigSyncResult(`finished sync, no updates for ${this.pb_idle} seconds`);
904
+ setConfigSyncResult(`sync continuing, but no update for ${this.pb_idle} seconds`);
902
905
  }
903
906
  }
904
907
  // if we get to 100, the progress bar stops but SignalR or countdown timer completes the sync
@@ -1195,7 +1198,6 @@ export class TenantNode {
1195
1198
  //
1196
1199
  // Azure AD Graph API
1197
1200
  //
1198
- //groupsGet - GET /groups
1199
1201
  export async function groupsGet(instance: IPublicClientApplication, user: User | undefined, groupSearchString: string): Promise<{ groups: Group[], error: string }> {
1200
1202
  // need a logged in user to get graph users
1201
1203
  if (user == null || user.spacode == "") {
@@ -1219,10 +1221,106 @@ export async function groupsGet(instance: IPublicClientApplication, user: User |
1219
1221
  return { groups: [], error: `Exception: ${error}` };
1220
1222
  }
1221
1223
  }
1224
+ export async function oauth2PermissionGrantsGet(options: RequestInit, spid: string, oid: string): Promise<{ grants: string, id: string, error: string }> {
1225
+ try {
1226
+ // make /oauth2PermissionGrants endpoint call
1227
+ let spurl: string = graphConfig.graphOauth2PermissionGrants;
1228
+ let url: URL = new URL(spurl);
1229
+ url.searchParams.append("$filter", `resourceId eq '${spid}' and consentType eq 'Principal' and principalId eq '${oid}'`);
1230
+ let response = await fetch(url.href, options);
1231
+ let data = await response.json();
1232
+ if (typeof data.error != "undefined") {
1233
+ return { grants: null, id: null, error: `${data.error.code}: ${data.error.message}` };
1234
+ }
1235
+ // we assume there is only one such grant
1236
+ if (data.value.length != 1) {
1237
+ debugger;
1238
+ return { grants: null, id: null, error: `oauth2PermissionGrantsGet: more than one matching delegated consent grant.` };
1239
+ }
1240
+ return { grants: data.value[0].scope, id: data.value[0].id, error: `` };
1241
+ }
1242
+ catch (error: any) {
1243
+ console.log(error);
1244
+ return { grants: null, id: null, error: `Exception: ${error}` };
1245
+ }
1246
+ }
1247
+ export async function oauth2PermissionGrantsSet(instance: IPublicClientApplication, loggedInUser: User, id: string, scopes: string): Promise<boolean> {
1248
+ // need a logged in user to get graph users
1249
+ if (loggedInUser == null || loggedInUser.spacode == "") {
1250
+ return false;
1251
+ }
1252
+ // make /oauth2PermissionGrants endpoint call
1253
+ try {
1254
+ let grantsurl: string = graphConfig.graphOauth2PermissionGrants + `/${id}`;
1255
+ let scopesBody: string = `{ "scope": "${scopes}" }`;
1256
+ const headers = await defineHeaders(instance, loggedInUser);
1257
+ let options: RequestInit = { method: "PATCH", headers: headers, body: scopesBody };
1258
+ let response = await fetch(grantsurl, options);
1259
+ if (response.status == 204 && response.statusText == "No Content") {
1260
+ return true;
1261
+ }
1262
+ else {
1263
+ debugger;
1264
+ console.log(`oauth2PermissionGrantsSet: PATCH failed ${data.error.code}: ${data.error.message}`);
1265
+ return false;
1266
+ }
1267
+ }
1268
+ catch (error: any) {
1269
+ debugger;
1270
+ console.log(error);
1271
+ return false;
1272
+ }
1273
+ }
1274
+ export function requestAdminConsent(user: User, scope: string): void {
1275
+ if (user.oid == "1") return;
1276
+ //
1277
+ // for app permissions (app roles) we must use the /.default scope for admin consent
1278
+ // https://learn.microsoft.com/EN-US/azure/active-directory/develop/v2-admin-consent#:~:text=In%20order%20to%20request%20app%20permissions%2C%20you%20must%20use%20the%20/.default%20value.
1279
+ // https://learn.microsoft.com/en-us/answers/questions/431784/how-to-grant-application-permissions-with-dynamic
1280
+ //
1281
+ // this means that, if we want to be granular about SyncReader vs. SyncWriter permissions, we must have separate Applications
1282
+ // for now, we request the /.default scope whenever any of the SyncReader or SyncWriter permissions are requested
1283
+ //
1284
+ // trying to use the Challenge endpoint for app permissions, setting this scope caused the call to quietly fail without error
1285
+ // scope = "63100afe-506e-4bb2-8ff7-d8d5ab373129/.default";
1286
+ //
1287
+ // thereforce, we are assuming that, for app permissions (app roles), the Microsoft Identity Web Challenge endpoint does not work and we need to perform our own redirect to the admin consent endpoint
1288
+ // https://learn.microsoft.com/EN-US/azure/active-directory/develop/scopes-oidc#client-credentials-grant-flow-and-default
1289
+ // https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#request-the-permissions-from-a-directory-admin
1290
+ //
1291
+ let adminConsentURL: string = "https://login.microsoftonline.com/";
1292
+ adminConsentURL += user.tid;
1293
+ adminConsentURL += "/adminconsent";
1294
+ let url: URL = new URL(adminConsentURL);
1295
+ url.searchParams.append("client_id", "63100afe-506e-4bb2-8ff7-d8d5ab373129");
1296
+ url.searchParams.append("redirect_uri", window.location.origin);
1297
+ url.searchParams.append("domain_hint", user.companyDomain);
1298
+ window.location.assign(url.href);
1299
+ }
1300
+ export async function servicePrincipalGet(options: RequestInit, appid: string): Promise<{ spid: string, error: string }> {
1301
+ try {
1302
+ // make /servicePrincipals endpoint call to get the Service Principal ID
1303
+ let spurl: string = graphConfig.graphServicePrincipalsEndpoint;
1304
+ spurl += `(appId='${appid}')`;
1305
+ let url: URL = new URL(spurl);
1306
+ url.searchParams.append("$select", "id,appId,displayName");
1307
+ let response = await fetch(url.href, options);
1308
+ let data = await response.json();
1309
+ if (typeof data.error !== "undefined") {
1310
+ return { spid: "", error: `${data.error.code}: ${data.error.message}` };
1311
+ }
1312
+ else {
1313
+ return { spid: data.id, error: `` };
1314
+ }
1315
+ }
1316
+ catch (error: any) {
1317
+ console.log(error);
1318
+ return { spid: "", error: `Exception: ${error}` };
1319
+ }
1320
+ }
1222
1321
  export async function signIn(user: User, tasks: TaskArray): Promise<boolean> {
1223
1322
  // admin authority is blank at signIn, lookup authority real-time
1224
1323
  if (user.authority == "") {
1225
- debugger;
1226
1324
  // lookup authority for this user (the lookup call does it based on domain, but TID works as well to find authority)
1227
1325
  let tenant: Tenant = new Tenant();
1228
1326
  tenant.domain = user.tid;
@@ -1239,7 +1337,7 @@ export async function signIn(user: User, tasks: TaskArray): Promise<boolean> {
1239
1337
  }
1240
1338
  switch (user.authority) {
1241
1339
  case graphConfig.authorityWW:
1242
- // SignIn by an admin consents the app, Challenge adds incremental permissions dynamicalyy, but requires a consented app
1340
+ // SignIn by an admin consents the app, Challenge adds incremental permissions dynamically, but requires a consented app
1243
1341
  let tenantURL: string = window.location.href;
1244
1342
  tenantURL += "MicrosoftIdentity/Account/SignIn";
1245
1343
  let url: URL = new URL(tenantURL);
@@ -1289,32 +1387,6 @@ export function signInIncrementally(user: User, scope: string): void {
1289
1387
  url.searchParams.append("loginHint", user.mail);
1290
1388
  window.location.assign(url.href);
1291
1389
  }
1292
- export function requestAdminConsent(user: User, scope: string): void {
1293
- if (user.oid == "1") return;
1294
- //
1295
- // for app permissions (app roles) we must use the /.default scope for admin consent
1296
- // https://learn.microsoft.com/EN-US/azure/active-directory/develop/v2-admin-consent#:~:text=In%20order%20to%20request%20app%20permissions%2C%20you%20must%20use%20the%20/.default%20value.
1297
- // https://learn.microsoft.com/en-us/answers/questions/431784/how-to-grant-application-permissions-with-dynamic
1298
- //
1299
- // this means that, if we want to be granular about SyncReader vs. SyncWriter permissions, we must have separate Applications
1300
- // for now, we request the /.default scope whenever any of the SyncReader or SyncWriter permissions are requested
1301
- //
1302
- // trying to use the Challenge endpoint for app permissions, setting this scope caused the call to quietly fail without error
1303
- // scope = "63100afe-506e-4bb2-8ff7-d8d5ab373129/.default";
1304
- //
1305
- // thereforce, we are assuming that, for app permissions (app roles), the Microsoft Identity Web Challenge endpoint does not work and we need to perform our own redirect to the admin consent endpoint
1306
- // https://learn.microsoft.com/EN-US/azure/active-directory/develop/scopes-oidc#client-credentials-grant-flow-and-default
1307
- // https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#request-the-permissions-from-a-directory-admin
1308
- //
1309
- let adminConsentURL: string = "https://login.microsoftonline.com/";
1310
- adminConsentURL += user.tid;
1311
- adminConsentURL += "/adminconsent";
1312
- let url: URL = new URL(adminConsentURL);
1313
- url.searchParams.append("client_id", "63100afe-506e-4bb2-8ff7-d8d5ab373129");
1314
- url.searchParams.append("redirect_uri", window.location.origin);
1315
- url.searchParams.append("domain_hint", user.companyDomain);
1316
- window.location.assign(url.href);
1317
- }
1318
1390
  export async function signOut(user: User): Promise<boolean>{
1319
1391
  if (user.oid == "1") return false;
1320
1392
  // set logout_hint in the .NET session for streamlined logout
@@ -1353,10 +1425,10 @@ export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant:
1353
1425
  try {
1354
1426
  let response: AuthenticationResult = await instance.acquireTokenByCode({ code: loggedInUser.spacode });
1355
1427
  loggedInUser.accessToken = response.accessToken; // cache access token on the user
1356
- console.log("Front end token acquired: " + loggedInUser.accessToken.slice(0, 20));
1428
+ console.log("tenantRelationshipsGetByDomain: Front end token acquired: " + loggedInUser.accessToken.slice(0, 20));
1357
1429
  }
1358
1430
  catch (error: any) {
1359
- console.log("Front end token failure: " + error);
1431
+ console.log("tenantRelationshipsGetByDomain: Front end token failure: " + error);
1360
1432
  return false; // failed to get access token, no need to re-render
1361
1433
  }
1362
1434
  }
@@ -1372,37 +1444,38 @@ export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant:
1372
1444
  tenantEndpoint += "(domainName='";
1373
1445
  tenantEndpoint += tenant.domain;
1374
1446
  tenantEndpoint += "')";
1375
- console.log("Attempting GET from /findTenantInformationByDomainName:", tenantEndpoint);
1447
+ console.log("tenantRelationshipsGetByDomain: Attempting GET from /findTenantInformationByDomainName:", tenantEndpoint);
1376
1448
  let response = await fetch(tenantEndpoint, options);
1377
1449
  if (response.status == 200 && response.statusText == "OK") {
1378
1450
  let data = await response.json();
1379
1451
  if (data) {
1380
1452
  if (data.error != null) {
1381
1453
  debugger;
1382
- console.log("Failed GET from /findTenantInformationByDomainName: ", data.error.message);
1454
+ console.log("tenantRelationshipsGetByDomain: Failed GET from /findTenantInformationByDomainName: ", data.error.message);
1383
1455
  return false;
1384
1456
  }
1385
1457
  else if (data.displayName != null && data.displayName !== "") {
1386
1458
  // set domain information on passed tenant
1387
1459
  tenant.tid = data.tenantId;
1388
1460
  tenant.name = data.displayName;
1389
- console.log("Successful GET from /findTenantInformationByDomainName: ", data.displayName);
1461
+ console.log("tenantRelationshipsGetByDomain: Successful GET from /findTenantInformationByDomainName: ", data.displayName);
1390
1462
  return true; // success, need UX to re-render
1391
1463
  }
1392
1464
  }
1393
1465
  else {
1394
- console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
1466
+ console.log("tenantRelationshipsGetByDomain: Failed to GET from /findTenantInformationByDomainName: ", tenantEndpoint);
1395
1467
  }
1396
1468
  }
1397
1469
  }
1398
1470
  catch (error: any) {
1399
- console.log("Failed to GET from /findTenantInformationByTenantId: ", error);
1471
+ console.log("Failed to GET from /findTenantInformationByDomainName: ", error);
1400
1472
  return false; // failed, no need for UX to re-render
1401
1473
  }
1402
1474
  return false; // failed, no need for UX to re-render
1403
1475
  }
1404
1476
  //tenantRelationshipsGetById - query AAD for associated company name and domain
1405
1477
  export async function tenantRelationshipsGetById(user: User, ii: InitInfo, instance: IPublicClientApplication, tasks: TaskArray, debug: boolean): Promise<boolean> {
1478
+ console.log("**** tenantRelationshipsGetById");
1406
1479
  if (debug) debugger;
1407
1480
  // since we should mainly be called when a user has newly logged in, we can afford the performance hit of looking up the tenant name and domain again
1408
1481
  // if (user.companyName != "") return false;
@@ -1411,10 +1484,10 @@ export async function tenantRelationshipsGetById(user: User, ii: InitInfo, insta
1411
1484
  try {
1412
1485
  let response: AuthenticationResult = await instance.acquireTokenByCode({ code: user.spacode });
1413
1486
  user.accessToken = response.accessToken; // cache access token
1414
- console.log("Front end token acquired: " + user.accessToken.slice(0, 20));
1487
+ console.log("tenantRelationshipsGetById: Front end token acquired: " + user.accessToken.slice(0, 20));
1415
1488
  }
1416
1489
  catch (error: any) {
1417
- console.log("Front end token failure: " + error);
1490
+ console.log("tenantRelationshipsGetById: Front end token failure: " + error);
1418
1491
  return false; // failed to get access token, no need to re-render
1419
1492
  }
1420
1493
  }
@@ -1432,7 +1505,7 @@ export async function tenantRelationshipsGetById(user: User, ii: InitInfo, insta
1432
1505
  tenantEndpoint += "')";
1433
1506
  // track time of tenant details query
1434
1507
  tasks.setTaskStart("GET tenant details", new Date());
1435
- console.log("Attempting GET from /findTenantInformationByTenantId:", tenantEndpoint);
1508
+ console.log("tenantRelationshipsGetById: Attempting GET from /findTenantInformationByTenantId:", tenantEndpoint);
1436
1509
  let response = await fetch(tenantEndpoint, options);
1437
1510
  let data = await response.json();
1438
1511
  if (data && typeof data.displayName !== undefined && data.displayName !== "") {
@@ -1449,16 +1522,16 @@ export async function tenantRelationshipsGetById(user: User, ii: InitInfo, insta
1449
1522
  console.log("tenantRelationshipsGetById: missing associated tenant for logged in user.");
1450
1523
  debugger;
1451
1524
  }
1452
- console.log("Successful GET from /findTenantInformationByTenantId: ", data.displayName);
1525
+ console.log("tenantRelationshipsGetById: Successful GET from /findTenantInformationByTenantId: ", data.displayName);
1453
1526
  tasks.setTaskEnd("GET tenant details", new Date(), "complete");
1454
1527
  return true; // success, need UX to re-render
1455
1528
  }
1456
1529
  else {
1457
- console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
1530
+ console.log("tenantRelationshipsGetById: Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
1458
1531
  }
1459
1532
  }
1460
1533
  catch (error: any) {
1461
- console.log("Failed to GET from /findTenantInformationByTenantId: ", error);
1534
+ console.log("tenantRelationshipsGetById: Failed to GET from /findTenantInformationByTenantId: ", error);
1462
1535
  tasks.setTaskEnd("GET tenant details", new Date(), "failed");
1463
1536
  return false; // failed, no need for UX to re-render
1464
1537
  }
@@ -1514,17 +1587,76 @@ export async function tenantUnauthenticatedLookup(tenant: Tenant, debug: boolean
1514
1587
  }
1515
1588
  return false; // failed, no need for UX to re-render
1516
1589
  }
1590
+ export async function userDelegatedScopesGet(instance: IPublicClientApplication, loggedInUser: User, tenant: Tenant): Promise<{ scopes: string, id: string, error: string }> {
1591
+ // need a logged in user and valid tenant to query graph
1592
+ if (loggedInUser == null || loggedInUser.spacode == "" || tenant == null) {
1593
+ debugger;
1594
+ return { scopes: null, id: null, error: `500: invalid parameter(s) passed to getUserDelegatedScopes` };
1595
+ }
1596
+ // create headers
1597
+ const headers = await defineHeaders(instance, loggedInUser);
1598
+ let options: RequestInit = { method: "GET", headers: headers };
1599
+ try {
1600
+ // first, cache Graph resource ID (service principal) for this tenant if we don't have it already
1601
+ if (tenant.graphSP == "") {
1602
+ let { spid, error } = await servicePrincipalGet(options, "00000003-0000-0000-c000-000000000000");
1603
+ if (error != "") {
1604
+ debugger;
1605
+ return { scopes: null, id: null, error: `${error}` };
1606
+ }
1607
+ tenant.graphSP = spid;
1608
+ }
1609
+ // then, retrieve the delegated Graph permissions assigned to this user
1610
+ let { grants, id, error } = await oauth2PermissionGrantsGet(options, tenant.graphSP, loggedInUser.oid);
1611
+ if (error != "") {
1612
+ debugger;
1613
+ return { scopes: null, id: null, error: `${error}` };
1614
+ }
1615
+ return { scopes: grants, id: id, error: `` };
1616
+ }
1617
+ catch (error: any) {
1618
+ debugger;
1619
+ console.log(error);
1620
+ return { scopes: null, id: null, error: `Exception: ${error}` };
1621
+ }
1622
+ }
1623
+ export async function userDelegatedScopesRemove(instance: IPublicClientApplication, loggedInUser: User, tenant: Tenant, scope: string): Promise<boolean> {
1624
+ // need a logged in user and valid tenant to query graph
1625
+ if (loggedInUser == null || loggedInUser.spacode == "" || tenant == null) {
1626
+ debugger;
1627
+ return false;
1628
+ }
1629
+ // get current set of delegated scopes in order to remove passed scope
1630
+ let { scopes, id, error } = await userDelegatedScopesGet(instance, loggedInUser, tenant);
1631
+ if (error != "") {
1632
+ debugger;
1633
+ console.log(`userDelegatedScopesRemove: cannot find userDelegatedScopes for ${loggedInUser.mail}: ${error}`);
1634
+ return false;
1635
+ }
1636
+ // remove passed scope (case sensitive)
1637
+ scopes = scopes.replace(scope, "");
1638
+ // set updated oauth2permissions
1639
+ let removed: boolean = await oauth2PermissionGrantsSet(instance, loggedInUser, id, scopes);
1640
+ if (!removed) {
1641
+ debugger;
1642
+ console.log(`userDelegatedScopesRemove: cannot set oauth2PermissionGrants for ${loggedInUser.mail}: ${error}`);
1643
+ return false;
1644
+ }
1645
+ // replace scope array on logged in user
1646
+ loggedInUser.scopes = scopes.split(" ");
1647
+ return removed;
1648
+ }
1517
1649
  //usersGet - GET from AAD Users endpoint
1518
1650
  export async function usersGet(instance: IPublicClientApplication, user: User | undefined): Promise<{ users: string[], error: string }> {
1519
1651
  // need a logged in user to get graph users
1520
1652
  if (user == null || user.spacode == "") {
1521
1653
  return { users: [], error: `500: invalid user passed to groupsGet` };
1522
1654
  }
1523
- // create headers
1524
- const headers = await defineHeaders(instance, user);
1525
- let options = { method: "GET", headers: headers };
1526
1655
  // make /users endpoint call
1527
1656
  try {
1657
+ // create headers
1658
+ const headers = await defineHeaders(instance, user);
1659
+ let options = { method: "GET", headers: headers };
1528
1660
  let response = await fetch(graphConfig.graphUsersEndpoint, options);
1529
1661
  let data = await response.json();
1530
1662
  if (typeof data.error !== "undefined") {
@@ -1541,9 +1673,7 @@ export async function usersGet(instance: IPublicClientApplication, user: User |
1541
1673
  return { users: [], error: `Exception: ${error}` };
1542
1674
  }
1543
1675
  }
1544
- //
1545
- // Mindline Config API
1546
- //
1676
+ // ======================= Mindline Config API ===============================
1547
1677
  export async function configEdit(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): Promise<APIResult> {
1548
1678
  let result: APIResult = new APIResult();
1549
1679
  if (config.id === "1") {
@@ -1587,6 +1717,7 @@ export async function configsRefresh(instance: IPublicClientApplication, authori
1587
1717
  }
1588
1718
  // retrieve Workspace(s), User(s), Tenant(s), Config(s) given newly logged in user
1589
1719
  export async function initGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, tasks: TaskArray, debug: boolean): Promise<APIResult> {
1720
+ console.log(`>>>>>> initGet`);
1590
1721
  let result: APIResult = new APIResult();
1591
1722
  if (debug) debugger;
1592
1723
  // lookup authority for this user (the lookup call does it based on domain, but TID works as well to find authority)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mindline/sync",
3
3
  "type": "module",
4
- "version": "1.0.56",
4
+ "version": "1.0.58",
5
5
  "types": "index.d.ts",
6
6
  "exports": "./index.ts",
7
7
  "description": "sync is a node.js package encapsulating javscript classes required for configuring Mindline sync service.",
package/tenants.json CHANGED
@@ -7,6 +7,7 @@
7
7
  "permissionType": "",
8
8
  "onboarded": false,
9
9
  "authority": "",
10
- "sel": true
10
+ "sel": true,
11
+ "graphSP": ""
11
12
  }
12
13
  ]
package/users.json CHANGED
@@ -3,11 +3,10 @@
3
3
  "oid": "1",
4
4
  "name": "",
5
5
  "mail": "",
6
- "authority": "",
6
+ "authority": "https://login.microsoftonline.com/",
7
7
  "tid": "",
8
8
  "companyName": "",
9
9
  "companyDomain": "",
10
- "associatedWorkspaces": [ "1" ],
11
10
  "session": "Sign In",
12
11
  "sel": true
13
12
  }
package/users2.json CHANGED
@@ -7,7 +7,6 @@
7
7
  "tid": "7f4567b8-f9a9-4ad3-9cb5-ef16a80e5744",
8
8
  "companyName": "Mindline1",
9
9
  "companyDomain": "mindline1.onmicrosoft.com",
10
- "associatedWorkspaces": [ "1", "2", "3" ],
11
10
  "session": "Sign In"
12
11
  },
13
12
  {
@@ -18,7 +17,6 @@
18
17
  "tid": "df9c2e0a-f6fe-43bb-a155-d51f66dffe0e",
19
18
  "companyName": "Mindline2",
20
19
  "companyDomain": "mindline2.onmicrosoft.com",
21
- "associatedWorkspaces": [ "1" ],
22
20
  "session": "Sign In"
23
21
  },
24
22
  {
@@ -29,7 +27,6 @@
29
27
  "tid": "1",
30
28
  "companyName": "WhoIAm",
31
29
  "companyDomain": "whoiam.onmicrosoft.com",
32
- "associatedWorkspaces": [ "2" ],
33
30
  "session": "Sign In"
34
31
  },
35
32
  {
@@ -40,7 +37,6 @@
40
37
  "tid": "2",
41
38
  "companyName": "Grit Sofware",
42
39
  "companyDomain": "gritsoftware.onmicrosoft.com",
43
- "associatedWorkspaces": [ "2" ],
44
40
  "session": "Sign In"
45
41
  },
46
42
  {
@@ -51,7 +47,6 @@
51
47
  "tid": "3",
52
48
  "companyName": "Google",
53
49
  "companyDomain": "google.onmicrosoft.com",
54
- "associatedWorkspaces": [ "3" ],
55
50
  "session": "Sign In"
56
51
  },
57
52
  {
@@ -62,7 +57,6 @@
62
57
  "tid": "4",
63
58
  "companyName": "Trackman Golf",
64
59
  "companyDomain": "trackman.onmicrosoft.com",
65
- "associatedWorkspaces": [ "3" ],
66
60
  "session": "Sign In"
67
61
  }
68
62
  ]