@backstage/plugin-catalog-backend 1.1.0-next.1 → 1.1.0

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,5 +1,94 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8012ac46a0: **BREAKING (alpha api):** Replace `createCatalogPolicyDecision` export with `createCatalogConditionalDecision`, which accepts a permission parameter of type `ResourcePermission<'catalog-entity'>` along with conditions. The permission passed is expected to be the handled permission in `PermissionPolicy#handle`, whose type must first be narrowed using methods like `isPermission` and `isResourcePermission`:
8
+
9
+ ```typescript
10
+ class TestPermissionPolicy implements PermissionPolicy {
11
+ async handle(
12
+ request: PolicyQuery<Permission>,
13
+ _user?: BackstageIdentityResponse,
14
+ ): Promise<PolicyDecision> {
15
+ if (
16
+ // Narrow type of `request.permission` to `ResourcePermission<'catalog-entity'>
17
+ isResourcePermission(request.permission, RESOURCE_TYPE_CATALOG_ENTITY)
18
+ ) {
19
+ return createCatalogConditionalDecision(
20
+ request.permission,
21
+ catalogConditions.isEntityOwner(
22
+ _user?.identity.ownershipEntityRefs ?? [],
23
+ ),
24
+ );
25
+ }
26
+
27
+ return {
28
+ result: AuthorizeResult.ALLOW,
29
+ };
30
+ ```
31
+
32
+ - 8012ac46a0: **BREAKING:** Mark CatalogBuilder#addPermissionRules as @alpha.
33
+ - fb02d2d94d: export `locationSpecToLocationEntity`
34
+ - bf82edf4c9: Added `/validate-entity` endpoint
35
+
36
+ ### Patch Changes
37
+
38
+ - ada4446733: Specify type of `visibilityPermission` property on collators and collator factories.
39
+ - 1691c6c5c2: Clarify that config locations that emit User and Group kinds now need to declare so in the `catalog.locations.[].rules`
40
+ - 8592cacfd3: Fixed an issue where sometimes entities would have stale relations "stuck" and
41
+ not getting removed as expected, after the other end of the relation had stopped
42
+ referring to them.
43
+ - 23646e51a5: Use new `PermissionEvaluator#authorizeConditional` method when retrieving permission conditions.
44
+ - 9fe24b0fc8: Adjust the error messages when entities fail validation, to clearly state what entity that failed it
45
+ - 48405ed232: Added `spec.profile.displayName` to search index for Group kinds
46
+ - 95408dbe99: Enable internal batching of very large deletions, to not run into SQL binding limits
47
+ - 8012ac46a0: Handle changes to @alpha permission-related types.
48
+
49
+ - All exported permission rules and conditions now have a `resourceType`.
50
+ - `createCatalogConditionalDecision` now expects supplied conditions to have the appropriate `resourceType`.
51
+ - `createCatalogPermissionRule` now expects `resourceType` as part of the supplied rule object.
52
+ - Introduce new `CatalogPermissionRule` convenience type.
53
+
54
+ - ffec894ed0: add gitlab to AnnotateScmSlugEntityProcessor
55
+ - Updated dependencies
56
+ - @backstage/integration@1.1.0
57
+ - @backstage/plugin-permission-common@0.6.0
58
+ - @backstage/plugin-permission-node@0.6.0
59
+ - @backstage/catalog-model@1.0.1
60
+ - @backstage/plugin-search-common@0.3.3
61
+ - @backstage/backend-common@0.13.2
62
+ - @backstage/plugin-catalog-common@1.0.1
63
+ - @backstage/catalog-client@1.0.1
64
+ - @backstage/plugin-scaffolder-common@1.0.1
65
+
66
+ ## 1.1.0-next.3
67
+
68
+ ### Patch Changes
69
+
70
+ - 23646e51a5: Use new `PermissionEvaluator#authorizeConditional` method when retrieving permission conditions.
71
+ - 48405ed232: Added `spec.profile.displayName` to search index for Group kinds
72
+ - Updated dependencies
73
+ - @backstage/plugin-permission-common@0.6.0-next.1
74
+ - @backstage/plugin-permission-node@0.6.0-next.2
75
+ - @backstage/backend-common@0.13.2-next.2
76
+ - @backstage/integration@1.1.0-next.2
77
+
78
+ ## 1.1.0-next.2
79
+
80
+ ### Minor Changes
81
+
82
+ - bf82edf4c9: Added `/validate-entity` endpoint
83
+
84
+ ### Patch Changes
85
+
86
+ - 8592cacfd3: Fixed an issue where sometimes entities would have stale relations "stuck" and
87
+ not getting removed as expected, after the other end of the relation had stopped
88
+ referring to them.
89
+ - Updated dependencies
90
+ - @backstage/catalog-model@1.0.1-next.1
91
+
3
92
  ## 1.1.0-next.1
4
93
 
5
94
  ### Minor Changes
package/README.md CHANGED
@@ -27,8 +27,7 @@ restoring the plugin, if you previously removed it.
27
27
 
28
28
  ```bash
29
29
  # From your Backstage root directory
30
- cd packages/backend
31
- yarn add @backstage/plugin-catalog-backend
30
+ yarn add --cwd packages/backend @backstage/plugin-catalog-backend
32
31
  ```
33
32
 
34
33
  ### Adding the plugin to your `packages/backend`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-catalog-backend",
3
- "version": "1.1.0-next.1",
3
+ "version": "1.1.0",
4
4
  "main": "../dist/index.cjs.js",
5
5
  "types": "../dist/index.alpha.d.ts"
6
6
  }
@@ -23,6 +23,7 @@ import { Permission } from '@backstage/plugin-permission-common';
23
23
  import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
24
24
  import { PermissionCondition } from '@backstage/plugin-permission-common';
25
25
  import { PermissionCriteria } from '@backstage/plugin-permission-common';
26
+ import { PermissionEvaluator } from '@backstage/plugin-permission-common';
26
27
  import { PermissionRule } from '@backstage/plugin-permission-node';
27
28
  import { PluginDatabaseManager } from '@backstage/backend-common';
28
29
  import { PluginEndpointDiscovery } from '@backstage/backend-common';
@@ -308,7 +309,7 @@ export declare type CatalogEnvironment = {
308
309
  database: PluginDatabaseManager;
309
310
  config: Config;
310
311
  reader: UrlReader;
311
- permissions: PermissionAuthorizer;
312
+ permissions: PermissionEvaluator | PermissionAuthorizer;
312
313
  };
313
314
 
314
315
  /**
@@ -572,8 +573,6 @@ export declare class DefaultCatalogCollatorFactory implements DocumentCollatorFa
572
573
  private constructor();
573
574
  getCollator(): Promise<Readable>;
574
575
  private applyArgsToFormat;
575
- private isUserEntity;
576
- private getDocumentText;
577
576
  private execute;
578
577
  }
579
578
 
@@ -23,6 +23,7 @@ import { Permission } from '@backstage/plugin-permission-common';
23
23
  import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
24
24
  import { PermissionCondition } from '@backstage/plugin-permission-common';
25
25
  import { PermissionCriteria } from '@backstage/plugin-permission-common';
26
+ import { PermissionEvaluator } from '@backstage/plugin-permission-common';
26
27
  import { PermissionRule } from '@backstage/plugin-permission-node';
27
28
  import { PluginDatabaseManager } from '@backstage/backend-common';
28
29
  import { PluginEndpointDiscovery } from '@backstage/backend-common';
@@ -287,7 +288,7 @@ export declare type CatalogEnvironment = {
287
288
  database: PluginDatabaseManager;
288
289
  config: Config;
289
290
  reader: UrlReader;
290
- permissions: PermissionAuthorizer;
291
+ permissions: PermissionEvaluator | PermissionAuthorizer;
291
292
  };
292
293
 
293
294
  /* Excluded from this release type: CatalogPermissionRule */
@@ -506,8 +507,6 @@ export declare class DefaultCatalogCollatorFactory implements DocumentCollatorFa
506
507
  private constructor();
507
508
  getCollator(): Promise<Readable>;
508
509
  private applyArgsToFormat;
509
- private isUserEntity;
510
- private getDocumentText;
511
510
  private execute;
512
511
  }
513
512
 
package/dist/index.cjs.js CHANGED
@@ -781,6 +781,24 @@ function createRandomProcessingInterval(options) {
781
781
  };
782
782
  }
783
783
 
784
+ function isUserEntity(entity) {
785
+ return entity.kind.toLocaleUpperCase("en-US") === "USER";
786
+ }
787
+ function isGroupEntity(entity) {
788
+ return entity.kind.toLocaleUpperCase("en-US") === "GROUP";
789
+ }
790
+ function getDocumentText(entity) {
791
+ var _a, _b;
792
+ const documentTexts = [];
793
+ documentTexts.push(entity.metadata.description || "");
794
+ if (isUserEntity(entity) || isGroupEntity(entity)) {
795
+ if ((_b = (_a = entity.spec) == null ? void 0 : _a.profile) == null ? void 0 : _b.displayName) {
796
+ documentTexts.push(entity.spec.profile.displayName);
797
+ }
798
+ }
799
+ return documentTexts.join(" : ");
800
+ }
801
+
784
802
  class DefaultCatalogCollatorFactory {
785
803
  constructor(options) {
786
804
  this.type = "software-catalog";
@@ -812,22 +830,6 @@ class DefaultCatalogCollatorFactory {
812
830
  }
813
831
  return formatted.toLowerCase();
814
832
  }
815
- isUserEntity(entity) {
816
- return entity.kind.toLocaleUpperCase("en-US") === "USER";
817
- }
818
- getDocumentText(entity) {
819
- var _a, _b, _c, _d, _e, _f;
820
- let documentText = entity.metadata.description || "";
821
- if (this.isUserEntity(entity)) {
822
- if (((_b = (_a = entity.spec) == null ? void 0 : _a.profile) == null ? void 0 : _b.displayName) && documentText) {
823
- const displayName = (_d = (_c = entity.spec) == null ? void 0 : _c.profile) == null ? void 0 : _d.displayName;
824
- documentText = displayName.concat(" : ", documentText);
825
- } else {
826
- documentText = ((_f = (_e = entity.spec) == null ? void 0 : _e.profile) == null ? void 0 : _f.displayName) || documentText;
827
- }
828
- }
829
- return documentText;
830
- }
831
833
  async *execute() {
832
834
  var _a, _b, _c, _d, _e, _f, _g;
833
835
  const { token } = await this.tokenManager.getToken();
@@ -849,7 +851,7 @@ class DefaultCatalogCollatorFactory {
849
851
  kind: entity.kind,
850
852
  name: entity.metadata.name
851
853
  }),
852
- text: this.getDocumentText(entity),
854
+ text: getDocumentText(entity),
853
855
  componentType: ((_c = (_b = entity.spec) == null ? void 0 : _b.type) == null ? void 0 : _c.toString()) || "other",
854
856
  type: ((_e = (_d = entity.spec) == null ? void 0 : _d.type) == null ? void 0 : _e.toString()) || "other",
855
857
  namespace: entity.metadata.namespace || "default",
@@ -1318,7 +1320,13 @@ class DefaultProcessingDatabase {
1318
1320
  entities: deferredEntities,
1319
1321
  sourceEntityRef: catalogModel.stringifyEntityRef(processedEntity)
1320
1322
  });
1321
- await tx("relations").where({ originating_entity_id: id }).delete();
1323
+ let previousRelationRows;
1324
+ if (tx.client.config.client.includes("sqlite3")) {
1325
+ previousRelationRows = await tx("relations").select("*").where({ originating_entity_id: id });
1326
+ await tx("relations").where({ originating_entity_id: id }).delete();
1327
+ } else {
1328
+ previousRelationRows = await tx("relations").where({ originating_entity_id: id }).delete().returning("*");
1329
+ }
1322
1330
  const relationRows = relations.map(({ source, target, type }) => ({
1323
1331
  originating_entity_id: id,
1324
1332
  source_entity_ref: catalogModel.stringifyEntityRef(source),
@@ -1326,6 +1334,11 @@ class DefaultProcessingDatabase {
1326
1334
  type
1327
1335
  }));
1328
1336
  await tx.batchInsert("relations", this.deduplicateRelations(relationRows), BATCH_SIZE$1);
1337
+ return {
1338
+ previous: {
1339
+ relations: previousRelationRows
1340
+ }
1341
+ };
1329
1342
  }
1330
1343
  async updateProcessedEntityErrors(txOpaque, options) {
1331
1344
  const tx = txOpaque;
@@ -1802,8 +1815,9 @@ class DefaultCatalogProcessingEngine {
1802
1815
  return;
1803
1816
  }
1804
1817
  result.completedEntity.metadata.uid = id;
1818
+ let oldRelationSources;
1805
1819
  await this.processingDatabase.transaction(async (tx) => {
1806
- await this.processingDatabase.updateProcessedEntity(tx, {
1820
+ const { previous } = await this.processingDatabase.updateProcessedEntity(tx, {
1807
1821
  id,
1808
1822
  processedEntity: result.completedEntity,
1809
1823
  resultHash,
@@ -1812,11 +1826,22 @@ class DefaultCatalogProcessingEngine {
1812
1826
  deferredEntities: result.deferredEntities,
1813
1827
  locationKey
1814
1828
  });
1829
+ oldRelationSources = new Set(previous.relations.map((r) => r.source_entity_ref));
1815
1830
  });
1831
+ const newRelationSources = new Set(result.relations.map((relation) => catalogModel.stringifyEntityRef(relation.source)));
1816
1832
  const setOfThingsToStitch = /* @__PURE__ */ new Set([
1817
- catalogModel.stringifyEntityRef(result.completedEntity),
1818
- ...result.relations.map((relation) => catalogModel.stringifyEntityRef(relation.source))
1833
+ catalogModel.stringifyEntityRef(result.completedEntity)
1819
1834
  ]);
1835
+ newRelationSources.forEach((r) => {
1836
+ if (!oldRelationSources.has(r)) {
1837
+ setOfThingsToStitch.add(r);
1838
+ }
1839
+ });
1840
+ oldRelationSources.forEach((r) => {
1841
+ if (!newRelationSources.has(r)) {
1842
+ setOfThingsToStitch.add(r);
1843
+ }
1844
+ });
1820
1845
  await this.stitcher.stitch(setOfThingsToStitch);
1821
1846
  track.markSuccessfulWithChanges(setOfThingsToStitch.size);
1822
1847
  } catch (error) {
@@ -2866,6 +2891,7 @@ async function createRouter(options) {
2866
2891
  entitiesCatalog,
2867
2892
  locationAnalyzer,
2868
2893
  locationService,
2894
+ orchestrator,
2869
2895
  refreshService,
2870
2896
  config,
2871
2897
  logger,
@@ -2988,6 +3014,46 @@ async function createRouter(options) {
2988
3014
  res.status(200).json(output);
2989
3015
  });
2990
3016
  }
3017
+ if (orchestrator) {
3018
+ router.post("/validate-entity", async (req, res) => {
3019
+ const bodySchema = zod.z.object({
3020
+ entity: zod.z.unknown(),
3021
+ location: zod.z.string()
3022
+ });
3023
+ let body;
3024
+ let entity;
3025
+ let location;
3026
+ try {
3027
+ body = await validateRequestBody(req, bodySchema);
3028
+ entity = validateEntityEnvelope(body.entity);
3029
+ location = catalogModel.parseLocationRef(body.location);
3030
+ if (location.type !== "url")
3031
+ throw new TypeError(`Invalid location ref ${body.location}, only 'url:<target>' is supported, e.g. url:https://host/path`);
3032
+ } catch (err) {
3033
+ return res.status(400).json({
3034
+ errors: [errors.serializeError(err)]
3035
+ });
3036
+ }
3037
+ const processingResult = await orchestrator.process({
3038
+ entity: {
3039
+ ...entity,
3040
+ metadata: {
3041
+ ...entity.metadata,
3042
+ annotations: {
3043
+ [catalogModel.ANNOTATION_LOCATION]: body.location,
3044
+ [catalogModel.ANNOTATION_ORIGIN_LOCATION]: body.location,
3045
+ ...entity.metadata.annotations
3046
+ }
3047
+ }
3048
+ }
3049
+ });
3050
+ if (!processingResult.ok)
3051
+ res.status(400).json({
3052
+ errors: processingResult.errors.map((e) => errors.serializeError(e))
3053
+ });
3054
+ return res.status(200).end();
3055
+ });
3056
+ }
2991
3057
  router.use(backendCommon.errorHandler());
2992
3058
  return router;
2993
3059
  }
@@ -3179,7 +3245,7 @@ class AuthorizedEntitiesCatalog {
3179
3245
  this.transformConditions = transformConditions;
3180
3246
  }
3181
3247
  async entities(request) {
3182
- const authorizeDecision = (await this.permissionApi.authorize([{ permission: pluginCatalogCommon.catalogEntityReadPermission }], { token: request == null ? void 0 : request.authorizationToken }))[0];
3248
+ const authorizeDecision = (await this.permissionApi.authorizeConditional([{ permission: pluginCatalogCommon.catalogEntityReadPermission }], { token: request == null ? void 0 : request.authorizationToken }))[0];
3183
3249
  if (authorizeDecision.result === pluginPermissionCommon.AuthorizeResult.DENY) {
3184
3250
  return {
3185
3251
  entities: [],
@@ -3196,7 +3262,7 @@ class AuthorizedEntitiesCatalog {
3196
3262
  return this.entitiesCatalog.entities(request);
3197
3263
  }
3198
3264
  async removeEntityByUid(uid, options) {
3199
- const authorizeResponse = (await this.permissionApi.authorize([{ permission: pluginCatalogCommon.catalogEntityDeletePermission }], { token: options == null ? void 0 : options.authorizationToken }))[0];
3265
+ const authorizeResponse = (await this.permissionApi.authorizeConditional([{ permission: pluginCatalogCommon.catalogEntityDeletePermission }], { token: options == null ? void 0 : options.authorizationToken }))[0];
3200
3266
  if (authorizeResponse.result === pluginPermissionCommon.AuthorizeResult.DENY) {
3201
3267
  throw new errors.NotAllowedError();
3202
3268
  }
@@ -3235,7 +3301,7 @@ class AuthorizedEntitiesCatalog {
3235
3301
  };
3236
3302
  }
3237
3303
  async facets(request) {
3238
- const authorizeDecision = (await this.permissionApi.authorize([{ permission: pluginCatalogCommon.catalogEntityReadPermission }], { token: request == null ? void 0 : request.authorizationToken }))[0];
3304
+ const authorizeDecision = (await this.permissionApi.authorizeConditional([{ permission: pluginCatalogCommon.catalogEntityReadPermission }], { token: request == null ? void 0 : request.authorizationToken }))[0];
3239
3305
  if (authorizeDecision.result === pluginPermissionCommon.AuthorizeResult.DENY) {
3240
3306
  return {
3241
3307
  facets: Object.fromEntries(request.facets.map((f) => [f, []]))
@@ -3408,7 +3474,14 @@ class CatalogBuilder {
3408
3474
  policy
3409
3475
  });
3410
3476
  const unauthorizedEntitiesCatalog = new DefaultEntitiesCatalog(dbClient);
3411
- const entitiesCatalog = new AuthorizedEntitiesCatalog(unauthorizedEntitiesCatalog, permissions, pluginPermissionNode.createConditionTransformer(this.permissionRules));
3477
+ let permissionEvaluator;
3478
+ if ("query" in permissions) {
3479
+ permissionEvaluator = permissions;
3480
+ } else {
3481
+ logger.warn("PermissionAuthorizer is deprecated. Please use an instance of PermissionEvaluator instead of PermissionAuthorizer in PluginEnvironment#permissions");
3482
+ permissionEvaluator = pluginPermissionCommon.toPermissionEvaluator(permissions);
3483
+ }
3484
+ const entitiesCatalog = new AuthorizedEntitiesCatalog(unauthorizedEntitiesCatalog, permissionEvaluator, pluginPermissionNode.createConditionTransformer(this.permissionRules));
3412
3485
  const permissionIntegrationRouter = pluginPermissionNode.createPermissionIntegrationRouter({
3413
3486
  resourceType: pluginCatalogCommon.RESOURCE_TYPE_CATALOG_ENTITY,
3414
3487
  getResources: async (resourceRefs) => {
@@ -3435,12 +3508,13 @@ class CatalogBuilder {
3435
3508
  const entityProviders = lodash__default["default"].uniqBy([...this.entityProviders, locationStore, configLocationProvider], (provider) => provider.getProviderName());
3436
3509
  const processingEngine = new DefaultCatalogProcessingEngine(logger, processingDatabase, orchestrator, stitcher, () => crypto.createHash("sha1"));
3437
3510
  const locationAnalyzer = (_b = this.locationAnalyzer) != null ? _b : new RepoLocationAnalyzer(logger, integrations);
3438
- const locationService = new AuthorizedLocationService(new DefaultLocationService(locationStore, orchestrator), permissions);
3439
- const refreshService = new AuthorizedRefreshService(new DefaultRefreshService({ database: processingDatabase }), permissions);
3511
+ const locationService = new AuthorizedLocationService(new DefaultLocationService(locationStore, orchestrator), permissionEvaluator);
3512
+ const refreshService = new AuthorizedRefreshService(new DefaultRefreshService({ database: processingDatabase }), permissionEvaluator);
3440
3513
  const router = await createRouter({
3441
3514
  entitiesCatalog,
3442
3515
  locationAnalyzer,
3443
3516
  locationService,
3517
+ orchestrator,
3444
3518
  refreshService,
3445
3519
  logger,
3446
3520
  config,