@backstage/integration 1.6.2 → 1.7.0-next.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/CHANGELOG.md CHANGED
@@ -1,19 +1,41 @@
1
1
  # @backstage/integration
2
2
 
3
- ## 1.6.2
3
+ ## 1.7.0-next.1
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - cf196443d8da: Additional fix for Gitiles auth links
7
+ - 2d2fc9d20ebb: Additional fix for Gitiles auth links
8
8
  - Updated dependencies
9
- - @backstage/config@1.0.8
9
+ - @backstage/config@1.1.0-next.0
10
10
  - @backstage/errors@1.2.1
11
11
 
12
- ## 1.6.1
12
+ ## 1.7.0-next.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 5f1a92b9f19f: Added `AzureDevOpsCredentialsProvider` to support multiple Azure DevOps organizations and **deprecated** `AzureIntegrationConfig.credential` and `AzureIntegrationConfig.token` in favour of `AzureIntegrationConfig.credentials`. You can now use specific credentials for different Azure DevOps (Server) organizations by specifying the `organizations` field on a credential:
17
+
18
+ ```yaml
19
+ integrations:
20
+ azure:
21
+ - host: dev.azure.com
22
+ credentials:
23
+ - organizations:
24
+ - my-org
25
+ - my-other-org
26
+ clientId: ${AZURE_CLIENT_ID}
27
+ clientSecret: ${AZURE_CLIENT_SECRET}
28
+ tenantId: ${AZURE_TENANT_ID}
29
+ - organizations:
30
+ - yet-another-org
31
+ personalAccessToken: ${PERSONAL_ACCESS_TOKEN}
32
+ ```
33
+
34
+ See the [Azure integration documentation](https://backstage.io/docs/integrations/azure/locations) for more information.
13
35
 
14
36
  ### Patch Changes
15
37
 
16
- - 842eb24ef5e8: Gitiles: Fixed auth prefix issue
38
+ - cb2e19d82d95: Gitiles: Fixed auth prefix issue
17
39
  - Updated dependencies
18
40
  - @backstage/config@1.0.8
19
41
  - @backstage/errors@1.2.1
package/config.d.ts CHANGED
@@ -30,8 +30,38 @@ export interface Config {
30
30
  /**
31
31
  * Token used to authenticate requests.
32
32
  * @visibility secret
33
+ * @deprecated Use `credentials` instead.
33
34
  */
34
35
  token?: string;
36
+
37
+ /**
38
+ * The credential to use for requests.
39
+ *
40
+ * If no credential is specified anonymous access is used.
41
+ *
42
+ * @visibility secret
43
+ * @deprecated Use `credentials` instead.
44
+ */
45
+ credential?: {
46
+ clientId?: string;
47
+ clientSecret?: string;
48
+ tenantId?: string;
49
+ personalAccessToken?: string;
50
+ };
51
+
52
+ /**
53
+ * The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used.
54
+ * If not organization matches the first credential without an organization is used.
55
+ *
56
+ * If no credentials are specified at all, either a default credential (for Azure DevOps) or anonymous access (for Azure DevOps Server) is used.
57
+ * @visibility secret
58
+ */
59
+ credentials?: {
60
+ clientId?: string;
61
+ clientSecret?: string;
62
+ tenantId?: string;
63
+ personalAccessToken?: string;
64
+ }[];
35
65
  }>;
36
66
 
37
67
  /**
package/dist/index.cjs.js CHANGED
@@ -121,10 +121,10 @@ function readAwsS3IntegrationConfigs(configs) {
121
121
  return result;
122
122
  }
123
123
 
124
- var __defProp$c = Object.defineProperty;
125
- var __defNormalProp$c = (obj, key, value) => key in obj ? __defProp$c(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
126
- var __publicField$c = (obj, key, value) => {
127
- __defNormalProp$c(obj, typeof key !== "symbol" ? key + "" : key, value);
124
+ var __defProp$d = Object.defineProperty;
125
+ var __defNormalProp$d = (obj, key, value) => key in obj ? __defProp$d(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
126
+ var __publicField$d = (obj, key, value) => {
127
+ __defNormalProp$d(obj, typeof key !== "symbol" ? key + "" : key, value);
128
128
  return value;
129
129
  };
130
130
  const _AwsS3Integration = class _AwsS3Integration {
@@ -148,7 +148,7 @@ const _AwsS3Integration = class _AwsS3Integration {
148
148
  return url;
149
149
  }
150
150
  };
151
- __publicField$c(_AwsS3Integration, "factory", ({ config }) => {
151
+ __publicField$d(_AwsS3Integration, "factory", ({ config }) => {
152
152
  var _a;
153
153
  const configs = readAwsS3IntegrationConfigs(
154
154
  (_a = config.getOptionalConfigArray("integrations.awsS3")) != null ? _a : []
@@ -350,43 +350,146 @@ _baseUrl = new WeakMap();
350
350
  let AzureUrl = _AzureUrl;
351
351
 
352
352
  const AZURE_HOST = "dev.azure.com";
353
- const isAzureClientSecretCredential = (credential) => {
354
- const clientSecretCredential = credential;
355
- return Object.keys(credential).length === 3 && clientSecretCredential.clientId !== void 0 && clientSecretCredential.clientSecret !== void 0 && clientSecretCredential.tenantId !== void 0;
356
- };
357
- const isAzureManagedIdentityCredential = (credential) => {
358
- return Object.keys(credential).length === 1 && credential.clientId !== void 0;
359
- };
353
+ const AzureDevOpsCredentialFields = [
354
+ "clientId",
355
+ "clientSecret",
356
+ "tenantId",
357
+ "personalAccessToken"
358
+ ];
359
+ const AzureDevopsCredentialFieldMap = /* @__PURE__ */ new Map([
360
+ ["ClientSecret", ["clientId", "clientSecret", "tenantId"]],
361
+ ["ManagedIdentity", ["clientId"]],
362
+ ["PersonalAccessToken", ["personalAccessToken"]]
363
+ ]);
364
+ function asAzureDevOpsCredential(credential) {
365
+ for (const entry of AzureDevopsCredentialFieldMap.entries()) {
366
+ const [kind, requiredFields] = entry;
367
+ const forbiddenFields = AzureDevOpsCredentialFields.filter(
368
+ (field) => !requiredFields.includes(field)
369
+ );
370
+ if (requiredFields.every((field) => credential[field] !== void 0) && forbiddenFields.every((field) => credential[field] === void 0)) {
371
+ return {
372
+ kind,
373
+ organizations: credential.organizations,
374
+ ...requiredFields.reduce((acc, field) => {
375
+ acc[field] = credential[field];
376
+ return acc;
377
+ }, {})
378
+ };
379
+ }
380
+ }
381
+ throw new Error("is not a valid credential");
382
+ }
360
383
  function readAzureIntegrationConfig(config) {
361
- var _a;
384
+ var _a, _b, _c, _d;
362
385
  const host = (_a = config.getOptionalString("host")) != null ? _a : AZURE_HOST;
386
+ let credentialConfigs = (_b = config.getOptionalConfigArray("credentials")) == null ? void 0 : _b.map((credential) => {
387
+ const result = {
388
+ organizations: credential.getOptionalStringArray("organizations"),
389
+ personalAccessToken: credential.getOptionalString(
390
+ "personalAccessToken"
391
+ ),
392
+ tenantId: credential.getOptionalString("tenantId"),
393
+ clientId: credential.getOptionalString("clientId"),
394
+ clientSecret: credential.getOptionalString("clientSecret")
395
+ };
396
+ return result;
397
+ });
363
398
  const token = config.getOptionalString("token");
364
- const credential = config.getOptional("credential") ? {
365
- tenantId: config.getOptionalString("credential.tenantId"),
366
- clientId: config.getOptionalString("credential.clientId"),
367
- clientSecret: config.getOptionalString("credential.clientSecret")
368
- } : void 0;
369
- if (!isValidHost(host)) {
399
+ if (config.getOptional("credential") !== void 0 && config.getOptional("credentials") !== void 0) {
370
400
  throw new Error(
371
- `Invalid Azure integration config, '${host}' is not a valid host`
401
+ `Invalid Azure integration config, 'credential' and 'credentials' cannot be used together. Use 'credentials' instead.`
372
402
  );
373
403
  }
374
- if (credential && !isAzureClientSecretCredential(credential) && !isAzureManagedIdentityCredential(credential)) {
404
+ if (config.getOptional("token") !== void 0 && config.getOptional("credentials") !== void 0) {
375
405
  throw new Error(
376
- `Invalid Azure integration config, credential is not valid`
406
+ `Invalid Azure integration config, 'token' and 'credentials' cannot be used together. Use 'credentials' instead.`
377
407
  );
378
408
  }
379
- if (credential && host !== AZURE_HOST) {
409
+ if (token !== void 0) {
410
+ const mapped = [{ personalAccessToken: token }];
411
+ credentialConfigs = (_c = credentialConfigs == null ? void 0 : credentialConfigs.concat(mapped)) != null ? _c : mapped;
412
+ }
413
+ if (config.getOptional("credential") !== void 0) {
414
+ const mapped = [
415
+ {
416
+ organizations: config.getOptionalStringArray(
417
+ "credential.organizations"
418
+ ),
419
+ token: config.getOptionalString("credential.token"),
420
+ tenantId: config.getOptionalString("credential.tenantId"),
421
+ clientId: config.getOptionalString("credential.clientId"),
422
+ clientSecret: config.getOptionalString("credential.clientSecret")
423
+ }
424
+ ];
425
+ credentialConfigs = (_d = credentialConfigs == null ? void 0 : credentialConfigs.concat(mapped)) != null ? _d : mapped;
426
+ }
427
+ if (!isValidHost(host)) {
380
428
  throw new Error(
381
- `Invalid Azure integration config, credential can only be used with ${AZURE_HOST}`
429
+ `Invalid Azure integration config, '${host}' is not a valid host`
382
430
  );
383
431
  }
384
- if (credential && token) {
385
- throw new Error(
386
- `Invalid Azure integration config, specify either a token or a credential but not both`
432
+ let credentials = void 0;
433
+ if (credentialConfigs !== void 0) {
434
+ const errors = credentialConfigs == null ? void 0 : credentialConfigs.reduce((acc, credentialConfig, index) => {
435
+ let error = void 0;
436
+ try {
437
+ asAzureDevOpsCredential(credentialConfig);
438
+ } catch (e) {
439
+ error = e.message;
440
+ }
441
+ if (error !== void 0) {
442
+ acc.push(`credential at position ${index + 1} ${error}`);
443
+ }
444
+ return acc;
445
+ }, Array.of()).concat(
446
+ Object.entries(
447
+ credentialConfigs.filter(
448
+ (credential) => credential.organizations !== void 0 && credential.organizations.length > 0
449
+ ).reduce((acc, credential, index) => {
450
+ var _a2;
451
+ (_a2 = credential.organizations) == null ? void 0 : _a2.forEach((organization) => {
452
+ if (!acc[organization]) {
453
+ acc[organization] = [];
454
+ }
455
+ acc[organization].push(index + 1);
456
+ });
457
+ return acc;
458
+ }, {})
459
+ ).filter(([_, indexes]) => indexes.length > 1).reduce((acc, [org, indexes]) => {
460
+ acc.push(
461
+ `organization ${org} is specified multiple times in credentials at positions ${indexes.slice(0, indexes.length - 1).join(", ")} and ${indexes[indexes.length - 1]}`
462
+ );
463
+ return acc;
464
+ }, Array.of())
387
465
  );
466
+ if ((errors == null ? void 0 : errors.length) > 0) {
467
+ throw new Error(
468
+ `Invalid Azure integration config for ${host}: ${errors.join("; ")}`
469
+ );
470
+ }
471
+ credentials = credentialConfigs.map(
472
+ (credentialConfig) => asAzureDevOpsCredential(credentialConfig)
473
+ );
474
+ if (credentials.some(
475
+ (credential) => credential.kind !== "PersonalAccessToken"
476
+ ) && host !== AZURE_HOST) {
477
+ throw new Error(
478
+ `Invalid Azure integration config for ${host}, only personal access tokens can be used with hosts other than ${AZURE_HOST}`
479
+ );
480
+ }
481
+ if (credentials.filter(
482
+ (credential) => credential.organizations === void 0 || credential.organizations.length === 0
483
+ ).length > 1) {
484
+ throw new Error(
485
+ `Invalid Azure integration config for ${host}, you cannot specify multiple credentials without organizations`
486
+ );
487
+ }
388
488
  }
389
- return { host, token, credential };
489
+ return {
490
+ host,
491
+ credentials
492
+ };
390
493
  }
391
494
  function readAzureIntegrationConfigs(configs) {
392
495
  const result = configs.map(readAzureIntegrationConfig);
@@ -396,10 +499,10 @@ function readAzureIntegrationConfigs(configs) {
396
499
  return result;
397
500
  }
398
501
 
399
- var __defProp$b = Object.defineProperty;
400
- var __defNormalProp$b = (obj, key, value) => key in obj ? __defProp$b(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
401
- var __publicField$b = (obj, key, value) => {
402
- __defNormalProp$b(obj, typeof key !== "symbol" ? key + "" : key, value);
502
+ var __defProp$c = Object.defineProperty;
503
+ var __defNormalProp$c = (obj, key, value) => key in obj ? __defProp$c(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
504
+ var __publicField$c = (obj, key, value) => {
505
+ __defNormalProp$c(obj, typeof key !== "symbol" ? key + "" : key, value);
403
506
  return value;
404
507
  };
405
508
  const _AzureIntegration = class _AzureIntegration {
@@ -447,7 +550,7 @@ const _AzureIntegration = class _AzureIntegration {
447
550
  return url;
448
551
  }
449
552
  };
450
- __publicField$b(_AzureIntegration, "factory", ({ config }) => {
553
+ __publicField$c(_AzureIntegration, "factory", ({ config }) => {
451
554
  var _a;
452
555
  const configs = readAzureIntegrationConfigs(
453
556
  (_a = config.getOptionalConfigArray("integrations.azure")) != null ? _a : []
@@ -468,29 +571,171 @@ function getAzureDownloadUrl(url) {
468
571
  function getAzureCommitsUrl(url) {
469
572
  return AzureUrl.fromRepoUrl(url).toCommitsUrl();
470
573
  }
574
+
575
+ var __defProp$b = Object.defineProperty;
576
+ var __defNormalProp$b = (obj, key, value) => key in obj ? __defProp$b(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
577
+ var __publicField$b = (obj, key, value) => {
578
+ __defNormalProp$b(obj, typeof key !== "symbol" ? key + "" : key, value);
579
+ return value;
580
+ };
581
+ const tenMinutes = 1e3 * 60 * 10;
582
+ class CachedAzureDevOpsCredentialsProvider {
583
+ constructor(credential) {
584
+ this.credential = credential;
585
+ __publicField$b(this, "azureDevOpsScope", "499b84ac-1321-427f-aa17-267ca6975798/.default");
586
+ __publicField$b(this, "cached");
587
+ }
588
+ static fromAzureDevOpsCredential(credential) {
589
+ switch (credential.kind) {
590
+ case "PersonalAccessToken":
591
+ return CachedAzureDevOpsCredentialsProvider.fromPersonalAccessTokenCredential(
592
+ credential
593
+ );
594
+ case "ClientSecret":
595
+ return CachedAzureDevOpsCredentialsProvider.fromTokenCredential(
596
+ new identity.ClientSecretCredential(
597
+ credential.tenantId,
598
+ credential.clientId,
599
+ credential.clientSecret
600
+ )
601
+ );
602
+ case "ManagedIdentity":
603
+ return CachedAzureDevOpsCredentialsProvider.fromTokenCredential(
604
+ new identity.ManagedIdentityCredential(credential.clientId)
605
+ );
606
+ default:
607
+ throw new Error(
608
+ `Credential kind '${credential.kind}' not supported`
609
+ );
610
+ }
611
+ }
612
+ static fromTokenCredential(credential) {
613
+ return new CachedAzureDevOpsCredentialsProvider(credential);
614
+ }
615
+ static fromPersonalAccessTokenCredential(credential) {
616
+ return new CachedAzureDevOpsCredentialsProvider(
617
+ credential.personalAccessToken
618
+ );
619
+ }
620
+ async getCredentials() {
621
+ if (this.cached === void 0 || this.cached.expiresAt !== void 0 && Date.now() > this.cached.expiresAt) {
622
+ if (typeof this.credential === "string") {
623
+ this.cached = {
624
+ headers: {
625
+ Authorization: `Basic ${btoa(`:${this.credential}`)}`
626
+ },
627
+ type: "pat",
628
+ token: this.credential
629
+ };
630
+ } else {
631
+ const accessToken = await this.credential.getToken(
632
+ this.azureDevOpsScope
633
+ );
634
+ if (!accessToken) {
635
+ throw new Error("Failed to retrieve access token");
636
+ }
637
+ this.cached = {
638
+ expiresAt: accessToken.expiresOnTimestamp - tenMinutes,
639
+ headers: {
640
+ Authorization: `Bearer ${accessToken.token}`
641
+ },
642
+ type: "bearer",
643
+ token: accessToken.token
644
+ };
645
+ }
646
+ }
647
+ return this.cached;
648
+ }
649
+ }
650
+
651
+ class DefaultAzureDevOpsCredentialsProvider {
652
+ constructor(providers) {
653
+ this.providers = providers;
654
+ }
655
+ static fromIntegrations(integrations) {
656
+ const providers = integrations.azure.list().reduce((acc, integration) => {
657
+ var _a;
658
+ (_a = integration.config.credentials) == null ? void 0 : _a.forEach((credential) => {
659
+ var _a2;
660
+ if (credential.organizations === void 0 || credential.organizations.length === 0) {
661
+ if (acc.get(integration.config.host) === void 0) {
662
+ acc.set(
663
+ integration.config.host,
664
+ CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential(
665
+ credential
666
+ )
667
+ );
668
+ }
669
+ } else {
670
+ const provider = CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential(
671
+ credential
672
+ );
673
+ (_a2 = credential.organizations) == null ? void 0 : _a2.forEach((organization) => {
674
+ acc.set(`${integration.config.host}/${organization}`, provider);
675
+ });
676
+ }
677
+ });
678
+ if (integration.config.host === "dev.azure.com" && acc.get(integration.config.host) === void 0) {
679
+ acc.set(
680
+ integration.config.host,
681
+ CachedAzureDevOpsCredentialsProvider.fromTokenCredential(
682
+ new identity.DefaultAzureCredential()
683
+ )
684
+ );
685
+ }
686
+ return acc;
687
+ }, /* @__PURE__ */ new Map());
688
+ return new DefaultAzureDevOpsCredentialsProvider(providers);
689
+ }
690
+ forAzureDevOpsServerOrganization(url) {
691
+ const parts = url.pathname.split("/").filter((part) => part !== "");
692
+ if (url.host !== "dev.azure.com" && parts.length > 0) {
693
+ if (parts[0] !== "tfs") {
694
+ return this.providers.get(`${url.host}/${parts[0]}`);
695
+ } else if (parts[0] === "tfs" && parts.length > 1) {
696
+ return this.providers.get(`${url.host}/${parts[1]}`);
697
+ }
698
+ }
699
+ return void 0;
700
+ }
701
+ forAzureDevOpsOrganization(url) {
702
+ const parts = url.pathname.split("/").filter((part) => part !== "");
703
+ if (url.host === "dev.azure.com" && parts.length > 0) {
704
+ return this.providers.get(`${url.host}/${parts[0]}`);
705
+ }
706
+ return void 0;
707
+ }
708
+ forHost(url) {
709
+ return this.providers.get(url.host);
710
+ }
711
+ async getCredentials(opts) {
712
+ var _a, _b;
713
+ const url = new URL(opts.url);
714
+ const provider = (_b = (_a = this.forAzureDevOpsOrganization(url)) != null ? _a : this.forAzureDevOpsServerOrganization(url)) != null ? _b : this.forHost(url);
715
+ if (provider === void 0) {
716
+ return void 0;
717
+ }
718
+ return provider.getCredentials(opts);
719
+ }
720
+ }
721
+
471
722
  async function getAzureRequestOptions(config, additionalHeaders) {
472
- const azureDevOpsScope = "499b84ac-1321-427f-aa17-267ca6975798/.default";
723
+ var _a;
473
724
  const headers = additionalHeaders ? { ...additionalHeaders } : {};
474
- const { token, credential } = config;
475
- if (credential) {
476
- if (isAzureClientSecretCredential(credential)) {
477
- const servicePrincipal = new identity.ClientSecretCredential(
478
- credential.tenantId,
479
- credential.clientId,
480
- credential.clientSecret
481
- );
482
- const accessToken = await servicePrincipal.getToken(azureDevOpsScope);
483
- headers.Authorization = `Bearer ${accessToken.token}`;
484
- } else if (isAzureManagedIdentityCredential(credential)) {
485
- const managedIdentity = new identity.ManagedIdentityCredential(
486
- credential.clientId
487
- );
488
- const accessToken = await managedIdentity.getToken(azureDevOpsScope);
489
- headers.Authorization = `Bearer ${accessToken.token}`;
490
- }
491
- } else if (token) {
492
- const buffer = Buffer.from(`:${config.token}`, "utf8");
493
- headers.Authorization = `Basic ${buffer.toString("base64")}`;
725
+ const credentialConfig = (_a = config.credentials) == null ? void 0 : _a.filter(
726
+ (credential) => credential.organizations === void 0 || credential.organizations.length === 0
727
+ )[0];
728
+ if (credentialConfig) {
729
+ const credentialsProvider = CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential(
730
+ credentialConfig
731
+ );
732
+ const credentials = await credentialsProvider.getCredentials();
733
+ return {
734
+ headers: {
735
+ ...credentials == null ? void 0 : credentials.headers,
736
+ ...headers
737
+ }
738
+ };
494
739
  }
495
740
  return { headers };
496
741
  }
@@ -1977,6 +2222,7 @@ exports.AzureIntegration = AzureIntegration;
1977
2222
  exports.BitbucketCloudIntegration = BitbucketCloudIntegration;
1978
2223
  exports.BitbucketIntegration = BitbucketIntegration;
1979
2224
  exports.BitbucketServerIntegration = BitbucketServerIntegration;
2225
+ exports.DefaultAzureDevOpsCredentialsProvider = DefaultAzureDevOpsCredentialsProvider;
1980
2226
  exports.DefaultGithubCredentialsProvider = DefaultGithubCredentialsProvider;
1981
2227
  exports.DefaultGitlabCredentialsProvider = DefaultGitlabCredentialsProvider;
1982
2228
  exports.GerritIntegration = GerritIntegration;