@red-hat-developer-hub/backstage-plugin-scorecard-backend 2.4.0 → 2.5.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 +38 -0
- package/README.md +89 -8
- package/config.d.ts +71 -3
- package/dist/constants/aggregationKPIs.cjs.js +6 -0
- package/dist/constants/aggregationKPIs.cjs.js.map +1 -0
- package/dist/database/DatabaseMetricValues.cjs.js +65 -0
- package/dist/database/DatabaseMetricValues.cjs.js.map +1 -1
- package/dist/middlewares/validateAggregationIdParam.cjs.js +20 -0
- package/dist/middlewares/validateAggregationIdParam.cjs.js.map +1 -0
- package/dist/middlewares/validateDatasourceQueryParams.cjs.js +18 -0
- package/dist/middlewares/validateDatasourceQueryParams.cjs.js.map +1 -0
- package/dist/middlewares/validateMetricIdsQueryParams.cjs.js +18 -0
- package/dist/middlewares/validateMetricIdsQueryParams.cjs.js.map +1 -0
- package/dist/permissions/permissionUtils.cjs.js +26 -0
- package/dist/permissions/permissionUtils.cjs.js.map +1 -1
- package/dist/permissions/rules.cjs.js +4 -1
- package/dist/permissions/rules.cjs.js.map +1 -1
- package/dist/plugin.cjs.js +10 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/providers/MetricProvidersRegistry.cjs.js +3 -0
- package/dist/providers/MetricProvidersRegistry.cjs.js.map +1 -1
- package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js +25 -3
- package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js.map +1 -1
- package/dist/service/CatalogMetricService.cjs.js +216 -8
- package/dist/service/CatalogMetricService.cjs.js.map +1 -1
- package/dist/service/mappers.cjs.js +14 -7
- package/dist/service/mappers.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +219 -92
- package/dist/service/router.cjs.js.map +1 -1
- package/dist/threshold/ThresholdEvaluator.cjs.js +2 -1
- package/dist/threshold/ThresholdEvaluator.cjs.js.map +1 -1
- package/dist/utils/buildAggregationConfig.cjs.js +15 -0
- package/dist/utils/buildAggregationConfig.cjs.js.map +1 -0
- package/dist/utils/mergeEntityAndProviderThresholds.cjs.js +3 -3
- package/dist/utils/mergeEntityAndProviderThresholds.cjs.js.map +1 -1
- package/dist/utils/metricUtils.cjs.js +35 -0
- package/dist/utils/metricUtils.cjs.js.map +1 -0
- package/dist/utils/normalizeOwnerRef.cjs.js +24 -0
- package/dist/utils/normalizeOwnerRef.cjs.js.map +1 -0
- package/dist/validation/validateAggregationConfig.cjs.js +47 -0
- package/dist/validation/validateAggregationConfig.cjs.js.map +1 -0
- package/dist/validation/validateDrillDownMetricsSchema.cjs.js +53 -0
- package/dist/validation/validateDrillDownMetricsSchema.cjs.js.map +1 -0
- package/migrations/20260217152637_add_entity_metadata_columns.js +37 -0
- package/package.json +14 -13
- package/dist/validation/validateCatalogMetricsSchema.cjs.js +0 -18
- package/dist/validation/validateCatalogMetricsSchema.cjs.js.map +0 -1
- package/dist/validation/validateMetricsSchema.cjs.js +0 -19
- package/dist/validation/validateMetricsSchema.cjs.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @red-hat-developer-hub/backstage-plugin-scorecard-backend
|
|
2
2
|
|
|
3
|
+
## 2.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d706601: Backstage version bump to v1.49.3
|
|
8
|
+
- 55226c2: Introduces custom threshold rule icons that can be configured in `app-config.yaml`.
|
|
9
|
+
- bd5f3e8: added disable metric feature to the pull metric task
|
|
10
|
+
- 243ad0a: Aggregated scorecards now use **aggregation IDs** and dedicated HTTP routes. The old catalog-aggregations URL still works but is **deprecated** (not removed).
|
|
11
|
+
|
|
12
|
+
**Backend (`@red-hat-developer-hub/backstage-plugin-scorecard-backend`)**
|
|
13
|
+
|
|
14
|
+
- **Deprecated:** `GET /metrics/:metricId/catalog/aggregations` — responses are unchanged, but the handler emits [RFC 8594](https://datatracker.ietf.org/doc/html/rfc8594) `Deprecation` and `Link` headers (alternate successor: `GET .../aggregations/:aggregationId`) and logs a warning. Prefer **`GET /aggregations/:aggregationId`** for new integrations.
|
|
15
|
+
- **Added:** `GET /aggregations/:aggregationId` for aggregated results using configured aggregation.
|
|
16
|
+
- **Added:** `GET /aggregations/:aggregationId/metadata` for KPI titles, descriptions, and aggregation metadata consumed by the UI.
|
|
17
|
+
|
|
18
|
+
**Common (`@red-hat-developer-hub/backstage-plugin-scorecard-common`)**
|
|
19
|
+
|
|
20
|
+
- Types and constants aligned with the aggregation config and new API shapes.
|
|
21
|
+
|
|
22
|
+
**Frontend (`@red-hat-developer-hub/backstage-plugin-scorecard`)**
|
|
23
|
+
|
|
24
|
+
- Homepage and aggregated flows resolve cards via **`aggregationId`**, fetch metadata from the new endpoint, and keep localized threshold and error strings where translation keys exist.
|
|
25
|
+
|
|
26
|
+
**Action for adopters:** Configure aggregated scorecards with `aggregationId` values that match backend aggregation config, replace direct calls to `GET /metrics/:metricId/catalog/aggregations` with `GET /aggregations/:aggregationId` (and metadata if you need the same labels as the plugin UI).
|
|
27
|
+
|
|
28
|
+
- c83b206: Adds the ability to drill down from aggregated scorecard KPIs to view the individual entities that contribute to the overall score. This enables managers and platform engineers to identify specific services impacting metrics and troubleshoot issues at the entity level.
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- 4f483a6: Normalize entity owners to their full canonical reference when syncing. Catalog entities are able to define a short name or the full entity reference for group ownership. This can lead to inconsistent filtering as we utilize the full entity reference for users and groups
|
|
33
|
+
- 89b273f: Adds the ability to sort by status that was originally missed during the initial implementation of the drill down feature
|
|
34
|
+
- Updated dependencies [d706601]
|
|
35
|
+
- Updated dependencies [55226c2]
|
|
36
|
+
- Updated dependencies [243ad0a]
|
|
37
|
+
- Updated dependencies [c83b206]
|
|
38
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-common@2.5.0
|
|
39
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-node@2.5.0
|
|
40
|
+
|
|
3
41
|
## 2.4.0
|
|
4
42
|
|
|
5
43
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -88,17 +88,23 @@ For more information about schedule configuration options, see the [Metric Colle
|
|
|
88
88
|
|
|
89
89
|
The following metric providers are available:
|
|
90
90
|
|
|
91
|
-
| Provider
|
|
92
|
-
|
|
|
93
|
-
| **GitHub**
|
|
94
|
-
| **Jira**
|
|
95
|
-
| **OpenSSF**
|
|
91
|
+
| Provider | Metric ID | Title | Description | Type |
|
|
92
|
+
| -------------- | ------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------ |
|
|
93
|
+
| **GitHub** | `github.open_prs` | GitHub open PRs | Count of open Pull Requests in GitHub | number |
|
|
94
|
+
| **Jira** | `jira.open_issues` | Jira open issues | The number of opened issues in Jira | number |
|
|
95
|
+
| **OpenSSF** | `openssf.*` | OpenSSF Security Scorecards | 18 security metrics from OpenSSF Scorecards (e.g., `openssf.code_review`, `openssf.maintained`). Each returns a score from 0-10. | number |
|
|
96
|
+
| **Dependabot** | `dependabot.*` | Dependabot Alerts | Critical, High, Medium and Low CVE Alerts | number |
|
|
96
97
|
|
|
97
98
|
To use these providers, install the corresponding backend modules:
|
|
98
99
|
|
|
99
100
|
- GitHub: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-github`](../scorecard-backend-module-github/README.md)
|
|
100
101
|
- Jira: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira`](../scorecard-backend-module-jira/README.md)
|
|
101
102
|
- OpenSSF: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf`](../scorecard-backend-module-openssf/README.md)
|
|
103
|
+
- Dependabot: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot`](../scorecard-backend-module-dependabot/README.md)
|
|
104
|
+
|
|
105
|
+
### Disabling Metrics
|
|
106
|
+
|
|
107
|
+
Administrators can disable metric checks globally via app-config, and users can disable them for concrete entity via `scorecard.io/disabled-metrics` annotation. For more details, see [disabled-metrics-logic.md](./docs/disabled-metrics-logic.md).
|
|
102
108
|
|
|
103
109
|
## Thresholds
|
|
104
110
|
|
|
@@ -112,6 +118,38 @@ Thresholds are evaluated in order, and the first matching rule determines the ca
|
|
|
112
118
|
|
|
113
119
|
For comprehensive threshold configuration guide, examples, and best practices, see [thresholds.md](./docs/thresholds.md).
|
|
114
120
|
|
|
121
|
+
## Aggregation KPIs (homepage and `GET /aggregations`)
|
|
122
|
+
|
|
123
|
+
Aggregated scorecard data for the authenticated user’s owned entities is exposed under **`GET /aggregations/:aggregationId`**. Optional entries in **`scorecard.aggregationKPIs`** assign a stable **aggregation id** (KPI key), custom **title** and **description**, **type**, and the backing **metricId**.
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
scorecard:
|
|
127
|
+
aggregationKPIs:
|
|
128
|
+
openIssuesKpi:
|
|
129
|
+
title: 'Jira open issues KPI'
|
|
130
|
+
description: 'Open issues across entities you own, grouped by status.'
|
|
131
|
+
type: statusGrouped
|
|
132
|
+
metricId: jira.open_issues
|
|
133
|
+
openPrsKpi:
|
|
134
|
+
title: 'GitHub open PRs KPI'
|
|
135
|
+
description: 'Open pull requests grouped by status.'
|
|
136
|
+
type: statusGrouped
|
|
137
|
+
metricId: github.open_prs
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
| Field | Description |
|
|
141
|
+
| ------------- | -------------------------------------------------------------- |
|
|
142
|
+
| `title` | Display title for this aggregation (returned in API metadata). |
|
|
143
|
+
| `description` | Display description for this aggregation. |
|
|
144
|
+
| `type` | Aggregation algorithm; currently `statusGrouped`. |
|
|
145
|
+
| `metricId` | Metric provider id used to load thresholds and compute counts. |
|
|
146
|
+
|
|
147
|
+
- **Path**: `scorecard.aggregationKPIs.<aggregationId>`.
|
|
148
|
+
- If **`aggregationKPIs` is omitted** or a given id is not listed, **`GET /aggregations/:aggregationId`** still works when **`aggregationId` equals the metric id** (e.g. `github.open_prs`): the backend uses that metric with the default `statusGrouped` aggregation and metric-defined title/description.
|
|
149
|
+
- Invalid KPI entries may be logged and skipped at runtime; prefer fixing config per backend validation.
|
|
150
|
+
|
|
151
|
+
**Homepage cards** are configured in the app (for example Dynamic Home Page mount points). They should pass **`aggregationId`** matching a key in `aggregationKPIs` or the metric id for the default case. See the [Scorecard frontend plugin README](../scorecard/README.md#homepage-scorecard-cards).
|
|
152
|
+
|
|
115
153
|
## API Endpoints
|
|
116
154
|
|
|
117
155
|
### `GET /metrics`
|
|
@@ -177,9 +215,52 @@ curl -X GET "{{url}}/api/scorecard/metrics/catalog/component/default/my-service?
|
|
|
177
215
|
-H "Authorization: Bearer <token>"
|
|
178
216
|
```
|
|
179
217
|
|
|
180
|
-
### `GET /
|
|
218
|
+
### `GET /aggregations/:aggregationId`
|
|
219
|
+
|
|
220
|
+
Returns aggregated metrics for the authenticated user across all catalog entities they own (same ownership rules as the legacy route; see [aggregation.md](./docs/aggregation.md)).
|
|
221
|
+
|
|
222
|
+
The **`aggregationId`** is either:
|
|
223
|
+
|
|
224
|
+
- A key under **`scorecard.aggregationKPIs`** in app-config (KPI-specific title, description, type, and `metricId`), or
|
|
225
|
+
- The **metric id** itself when no KPI entry exists (default **statusGrouped** behavior).
|
|
226
|
+
|
|
227
|
+
#### Path Parameters
|
|
228
|
+
|
|
229
|
+
| Parameter | Type | Required | Description |
|
|
230
|
+
| --------------- | ------ | -------- | ------------------------------------------------------------------------------------- |
|
|
231
|
+
| `aggregationId` | string | Yes | KPI id from `scorecard.aggregationKPIs` or the metric id for the default aggregation. |
|
|
232
|
+
|
|
233
|
+
#### Authentication / permissions
|
|
234
|
+
|
|
235
|
+
Same as the deprecated aggregation route: user authentication, `scorecard.metric.read`, and `catalog.entity.read` for each aggregated entity.
|
|
236
|
+
|
|
237
|
+
#### Example Request
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
curl -X GET "{{url}}/api/scorecard/aggregations/openIssuesKpi" \
|
|
241
|
+
-H "Authorization: Bearer <token>"
|
|
242
|
+
|
|
243
|
+
# Default aggregation when no KPI is configured (id equals metric id)
|
|
244
|
+
curl -X GET "{{url}}/api/scorecard/aggregations/github.open_prs" \
|
|
245
|
+
-H "Authorization: Bearer <token>"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### `GET /aggregations/:aggregationId/metadata`
|
|
249
|
+
|
|
250
|
+
Returns **title**, **description**, **type**, **history**, and **aggregationType** for the aggregation without computing full aggregate counts. Uses the same resolution rules as `GET /aggregations/:aggregationId` (KPI config vs metric id fallback).
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
curl -X GET "{{url}}/api/scorecard/aggregations/openIssuesKpi/metadata" \
|
|
254
|
+
-H "Authorization: Bearer <token>"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### `GET /metrics/:metricId/catalog/aggregations` (deprecated; removal planned)
|
|
258
|
+
|
|
259
|
+
This endpoint **remains available** for backward compatibility and behaves like the default case of **`GET /aggregations/:metricId`** (status-grouped aggregation for that metric). **It will be removed in a future major release** of the plugin - migrate to **`GET /aggregations/:aggregationId`**.
|
|
260
|
+
|
|
261
|
+
Responses include **RFC 8594** deprecation signals: header **`Deprecation: true`** and **`Link: <…/aggregations/<metricId>>; rel="alternate"`** pointing at the successor URL. The backend also logs a warning when this route is used.
|
|
181
262
|
|
|
182
|
-
Returns aggregated metrics for a specific metric across all entities owned by the authenticated user
|
|
263
|
+
Returns aggregated metrics for a specific metric across all entities owned by the authenticated user:
|
|
183
264
|
|
|
184
265
|
- Entities directly owned by the user
|
|
185
266
|
- Entities owned by groups the user is a direct member of (only direct parent groups are considered)
|
|
@@ -204,7 +285,7 @@ Requires `scorecard.metric.read` permission. Additionally:
|
|
|
204
285
|
#### Example Request
|
|
205
286
|
|
|
206
287
|
```bash
|
|
207
|
-
#
|
|
288
|
+
# Deprecated — prefer GET /aggregations/github.open_prs (or your KPI id)
|
|
208
289
|
curl -X GET "{{url}}/api/scorecard/metrics/github.open_prs/catalog/aggregations" \
|
|
209
290
|
-H "Authorization: Bearer <token>"
|
|
210
291
|
```
|
package/config.d.ts
CHANGED
|
@@ -15,12 +15,39 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { SchedulerServiceTaskScheduleDefinitionConfig } from '@backstage/backend-plugin-api';
|
|
18
|
+
import { AggregationType } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';
|
|
18
19
|
|
|
19
20
|
export interface Config {
|
|
20
21
|
/** Configuration for scorecard plugin */
|
|
21
22
|
scorecard?: {
|
|
23
|
+
/** Configuration for scorecard aggregation KPIs */
|
|
24
|
+
aggregationKPIs?: {
|
|
25
|
+
/** Unique identifier for scorecard aggregation KPIs */
|
|
26
|
+
[aggregationId: string]: {
|
|
27
|
+
/** Title of the aggregation */
|
|
28
|
+
title: string;
|
|
29
|
+
/** Description of the aggregation */
|
|
30
|
+
description: string;
|
|
31
|
+
/** Type of the aggregation */
|
|
32
|
+
type: AggregationType;
|
|
33
|
+
/** Metric ID for which the aggregation is calculated */
|
|
34
|
+
metricId: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
22
37
|
/** Number of days to retain metric data in the database. Older data will be automatically cleaned up. Default: 365 days */
|
|
23
38
|
dataRetentionDays?: number;
|
|
39
|
+
/** List of metric IDs (e.g. openssf.packaging) that are disabled globally. Entity annotations cannot override this. */
|
|
40
|
+
disabledMetrics?: string[];
|
|
41
|
+
/** Control whether users can override behavior via entity annotations. */
|
|
42
|
+
entityAnnotations?: {
|
|
43
|
+
/** Whether entity scorecard.io/disabled-metrics annotation can override. Only affects annotations; global disabledMetrics is unchanged. */
|
|
44
|
+
disabledMetrics?: {
|
|
45
|
+
/** If true (default), entities can disable metrics that are not mentioned in `except` list via `scorecard.io/disabled-metrics` annotation; if false, the annotation has no effect */
|
|
46
|
+
enabled?: boolean;
|
|
47
|
+
/** When enabled is true: entity annotations cannot disable metric IDs listed here (these checks always run). */
|
|
48
|
+
except?: string[];
|
|
49
|
+
};
|
|
50
|
+
};
|
|
24
51
|
/** Configuration for scorecard metric providers */
|
|
25
52
|
plugins?: {
|
|
26
53
|
/** Configuration for datasource */
|
|
@@ -31,17 +58,58 @@ export interface Config {
|
|
|
31
58
|
[metricName: string]: {
|
|
32
59
|
/** Threshold configuration for the metric */
|
|
33
60
|
thresholds?: {
|
|
61
|
+
/**
|
|
62
|
+
* Rules describe how metric values are categorized and how that category is presented in the UI.
|
|
63
|
+
* They are evaluated in order and the first matching rule is applied.
|
|
64
|
+
*/
|
|
34
65
|
rules?: Array<{
|
|
66
|
+
/**
|
|
67
|
+
* Threshold category key that a metric value is assigned to when this rule
|
|
68
|
+
* matches (for example `success`, `warning`, `error`, or a custom key).
|
|
69
|
+
*/
|
|
35
70
|
key: string;
|
|
36
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Threshold expression that determines whether a metric value matches this
|
|
73
|
+
* rule. Supports:`>=`, `<=`, `>`, `<`, `==`, `!=`, `-` (range).
|
|
74
|
+
*
|
|
75
|
+
* @example `<= 10` - Metric value must be less than or equal to 10.
|
|
76
|
+
* @example `10-60` - Metric value must be between 10 and 60 (inclusive).
|
|
77
|
+
*/
|
|
37
78
|
expression: string;
|
|
38
79
|
/**
|
|
39
|
-
* Color
|
|
40
|
-
*
|
|
80
|
+
* Color configuration - supports multiple formats:
|
|
81
|
+
* - theme palette reference (`success.main` / `warning.main` / `error.main`)
|
|
82
|
+
* - HEX code (e.g. '#FFA500')
|
|
83
|
+
* - RGB/RGBA (e.g. 'rgb(255, 0, 0)')
|
|
84
|
+
*
|
|
85
|
+
* Threshold rules 'success', 'warning' and 'error' have default colors.
|
|
41
86
|
*/
|
|
42
87
|
color?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Icon configuration - supports multiple formats:
|
|
90
|
+
* - Backstage system icons: 'kind:component', 'kind:api', etc.
|
|
91
|
+
* - Material Design icons: 'settings', 'home', 'build', etc.
|
|
92
|
+
* - SVG strings: '<svg xmlns="http://www.w3.org/2000/svg">...</svg>'
|
|
93
|
+
* - URLs: 'https://example.com/icon.png', '/assets/icon.svg'
|
|
94
|
+
* - Data URIs: 'data:image/svg+xml;base64,...'
|
|
95
|
+
*
|
|
96
|
+
* Threshold rules 'success', 'warning' and 'error' have default icons.
|
|
97
|
+
*/
|
|
98
|
+
icon?: string;
|
|
43
99
|
}>;
|
|
44
100
|
};
|
|
101
|
+
/**
|
|
102
|
+
* Schedule for collecting this metric. If not set, the default hourly schedule is used.
|
|
103
|
+
*
|
|
104
|
+
* Default schedule:
|
|
105
|
+
* ```ts
|
|
106
|
+
* {
|
|
107
|
+
* frequency: { hours: 1 },
|
|
108
|
+
* timeout: { minutes: 15 },
|
|
109
|
+
* initialDelay: { minutes: 1 },
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
45
113
|
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
|
|
46
114
|
};
|
|
47
115
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregationKPIs.cjs.js","sources":["../../src/constants/aggregationKPIs.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const AGGREGATION_KPIS_CONFIG_PATH =\n 'scorecard.aggregationKPIs' as const;\n"],"names":[],"mappings":";;AAgBO,MAAM,4BACX,GAAA;;;;"}
|
|
@@ -4,6 +4,7 @@ class DatabaseMetricValues {
|
|
|
4
4
|
constructor(dbClient) {
|
|
5
5
|
this.dbClient = dbClient;
|
|
6
6
|
}
|
|
7
|
+
dbClient;
|
|
7
8
|
tableName = "metric_values";
|
|
8
9
|
/**
|
|
9
10
|
* Insert multiple metric values
|
|
@@ -66,6 +67,70 @@ class DatabaseMetricValues {
|
|
|
66
67
|
statusCounts
|
|
67
68
|
};
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Fetch the latest entity metric values for a given metric, with optional filtering
|
|
72
|
+
* by status, name, kind, namespace, or owner, plus sorting and pagination.
|
|
73
|
+
*/
|
|
74
|
+
async readEntityMetricsWithFilters(metric_id, options) {
|
|
75
|
+
const clientName = this.dbClient.client?.config?.client ?? "";
|
|
76
|
+
const isPostgres = clientName === "pg" || clientName.includes("postgres");
|
|
77
|
+
const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metric_id).groupBy("catalog_entity_ref");
|
|
78
|
+
const query = this.dbClient(this.tableName).select("*").whereIn("id", latestIdsSubquery);
|
|
79
|
+
const sortColumnMap = {
|
|
80
|
+
entityName: "catalog_entity_ref",
|
|
81
|
+
owner: "entity_owner",
|
|
82
|
+
entityKind: "entity_kind",
|
|
83
|
+
timestamp: "timestamp",
|
|
84
|
+
metricValue: "value",
|
|
85
|
+
namespace: "entity_namespace",
|
|
86
|
+
status: "status"
|
|
87
|
+
};
|
|
88
|
+
const column = (options.sortBy && sortColumnMap[options.sortBy]) ?? "timestamp";
|
|
89
|
+
const direction = options.sortOrder === "asc" ? "asc" : "desc";
|
|
90
|
+
this.applySort(query, options.sortBy, column, direction, isPostgres);
|
|
91
|
+
if (options.status) {
|
|
92
|
+
query.where("status", options.status);
|
|
93
|
+
}
|
|
94
|
+
if (options.entityName) {
|
|
95
|
+
const escaped = options.entityName.replace(/[%_\\]/g, "\\$&");
|
|
96
|
+
query.whereRaw("catalog_entity_ref LIKE ? ESCAPE '\\'", [`%${escaped}%`]);
|
|
97
|
+
}
|
|
98
|
+
if (options.entityKind) {
|
|
99
|
+
query.where("entity_kind", options.entityKind);
|
|
100
|
+
}
|
|
101
|
+
if (options.entityNamespace) {
|
|
102
|
+
query.where("entity_namespace", options.entityNamespace);
|
|
103
|
+
}
|
|
104
|
+
if (options.entityOwner && options.entityOwner.length > 0) {
|
|
105
|
+
query.whereIn("entity_owner", options.entityOwner);
|
|
106
|
+
}
|
|
107
|
+
if (options.pagination) {
|
|
108
|
+
query.limit(options.pagination.limit).offset(options.pagination.offset);
|
|
109
|
+
}
|
|
110
|
+
return await query;
|
|
111
|
+
}
|
|
112
|
+
applySort(query, sortBy, column, direction, isPostgres) {
|
|
113
|
+
if (sortBy === "metricValue") {
|
|
114
|
+
if (isPostgres) {
|
|
115
|
+
query.orderByRaw(
|
|
116
|
+
`CAST(value::text AS DOUBLE PRECISION) ${direction} NULLS LAST, id ASC`
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
query.orderByRaw(
|
|
120
|
+
`value IS NULL, CAST(CAST(value AS TEXT) AS REAL) ${direction}, id ASC`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
} else if (sortBy === "status") {
|
|
124
|
+
if (isPostgres) {
|
|
125
|
+
query.orderByRaw(`status ${direction} NULLS LAST, id ASC`);
|
|
126
|
+
} else {
|
|
127
|
+
query.orderByRaw(`status IS NULL, status ${direction}, id ASC`);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
query.orderBy(column, direction);
|
|
131
|
+
query.orderBy("id", "asc");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
69
134
|
}
|
|
70
135
|
|
|
71
136
|
exports.DatabaseMetricValues = DatabaseMetricValues;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DatabaseMetricValues.cjs.js","sources":["../../src/database/DatabaseMetricValues.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Knex } from 'knex';\nimport {\n DbMetricValueCreate,\n DbMetricValue,\n DbAggregatedMetric,\n} from './types';\n\nexport class DatabaseMetricValues {\n private readonly tableName = 'metric_values';\n\n constructor(private readonly dbClient: Knex<any, any[]>) {}\n\n /**\n * Insert multiple metric values\n */\n async createMetricValues(metricValues: DbMetricValueCreate[]): Promise<void> {\n if (metricValues.length === 0) {\n return;\n }\n await this.dbClient(this.tableName).insert(metricValues);\n }\n\n /**\n * Get the latest metric values for a specific entity and metrics\n */\n async readLatestEntityMetricValues(\n catalog_entity_ref: string,\n metric_ids: string[],\n ): Promise<DbMetricValue[]> {\n return await this.dbClient(this.tableName)\n .select('*')\n .whereIn(\n 'id',\n this.dbClient(this.tableName)\n .max('id')\n .whereIn('metric_id', metric_ids)\n .where('catalog_entity_ref', catalog_entity_ref)\n .groupBy('metric_id'),\n );\n }\n\n /**\n * Delete metric values that are older than the given date\n */\n async cleanupExpiredMetrics(olderThan: Date): Promise<number> {\n return await this.dbClient(this.tableName)\n .where('timestamp', '<', olderThan)\n .del();\n }\n\n /**\n * Get aggregated metrics by status for multiple entities and metrics.\n */\n async readAggregatedMetricByEntityRefs(\n catalog_entity_refs: string[],\n metric_id: string,\n ): Promise<DbAggregatedMetric | undefined> {\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .whereIn('catalog_entity_ref', catalog_entity_refs)\n .groupBy('catalog_entity_ref');\n\n const statusRows = await this.dbClient(this.tableName)\n .select('status')\n .count('* as count')\n .max('timestamp as max_timestamp')\n .whereIn('id', latestIdsSubquery)\n .whereNotNull('status')\n .whereNotNull('value')\n .groupBy('status');\n\n if (!statusRows || statusRows.length === 0) {\n return undefined;\n }\n\n // Normalize types for cross-database compatibility\n // PostgreSQL returns COUNT/SUM as strings, SQLite returns numbers\n // PostgreSQL returns MAX(timestamp) as Date, SQLite returns number (milliseconds)\n const normalizeTimestamp = (timestamp: any): Date => {\n if (timestamp instanceof Date) {\n return timestamp;\n } else if (\n typeof timestamp === 'number' ||\n typeof timestamp === 'string'\n ) {\n return new Date(timestamp);\n }\n return new Date();\n };\n\n let maxTimestamp = new Date(0);\n let total = 0;\n const statusCounts: Record<string, number> = {};\n for (const row of statusRows) {\n const rowTimestamp = normalizeTimestamp(row.max_timestamp);\n if (rowTimestamp > maxTimestamp) {\n maxTimestamp = rowTimestamp;\n }\n const name = row.status as string;\n const count = Number(row.count);\n statusCounts[name] = count;\n total += count;\n }\n\n return {\n metric_id,\n total,\n max_timestamp: maxTimestamp,\n statusCounts,\n };\n }\n}\n"],"names":[],"mappings":";;AAuBO,MAAM,oBAAqB,CAAA;AAAA,EAGhC,YAA6B,QAA4B,EAAA;AAA5B,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA;AAA6B,EAFzC,SAAY,GAAA,eAAA;AAAA;AAAA;AAAA;AAAA,EAO7B,MAAM,mBAAmB,YAAoD,EAAA;AAC3E,IAAI,IAAA,YAAA,CAAa,WAAW,CAAG,EAAA;AAC7B,MAAA;AAAA;AAEF,IAAA,MAAM,KAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CAAA,CAAE,OAAO,YAAY,CAAA;AAAA;AACzD;AAAA;AAAA;AAAA,EAKA,MAAM,4BACJ,CAAA,kBAAA,EACA,UAC0B,EAAA;AAC1B,IAAO,OAAA,MAAM,KAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CACtC,CAAA,MAAA,CAAO,GAAG,CACV,CAAA,OAAA;AAAA,MACC,IAAA;AAAA,MACA,KAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CAAA,CACzB,IAAI,IAAI,CAAA,CACR,OAAQ,CAAA,WAAA,EAAa,UAAU,CAC/B,CAAA,KAAA,CAAM,sBAAsB,kBAAkB,CAAA,CAC9C,QAAQ,WAAW;AAAA,KACxB;AAAA;AACJ;AAAA;AAAA;AAAA,EAKA,MAAM,sBAAsB,SAAkC,EAAA;AAC5D,IAAO,OAAA,MAAM,IAAK,CAAA,QAAA,CAAS,IAAK,CAAA,SAAS,CACtC,CAAA,KAAA,CAAM,WAAa,EAAA,GAAA,EAAK,SAAS,CAAA,CACjC,GAAI,EAAA;AAAA;AACT;AAAA;AAAA;AAAA,EAKA,MAAM,gCACJ,CAAA,mBAAA,EACA,SACyC,EAAA;AACzC,IAAA,MAAM,oBAAoB,IAAK,CAAA,QAAA,CAAS,KAAK,SAAS,CAAA,CACnD,IAAI,IAAI,CAAA,CACR,KAAM,CAAA,WAAA,EAAa,SAAS,CAC5B,CAAA,OAAA,CAAQ,sBAAsB,mBAAmB,CAAA,CACjD,QAAQ,oBAAoB,CAAA;AAE/B,IAAM,MAAA,UAAA,GAAa,MAAM,IAAA,CAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CAAA,CAClD,MAAO,CAAA,QAAQ,CACf,CAAA,KAAA,CAAM,YAAY,CAAA,CAClB,GAAI,CAAA,4BAA4B,CAChC,CAAA,OAAA,CAAQ,IAAM,EAAA,iBAAiB,CAC/B,CAAA,YAAA,CAAa,QAAQ,CAAA,CACrB,YAAa,CAAA,OAAO,CACpB,CAAA,OAAA,CAAQ,QAAQ,CAAA;AAEnB,IAAA,IAAI,CAAC,UAAA,IAAc,UAAW,CAAA,MAAA,KAAW,CAAG,EAAA;AAC1C,MAAO,OAAA,MAAA;AAAA;AAMT,IAAM,MAAA,kBAAA,GAAqB,CAAC,SAAyB,KAAA;AACnD,MAAA,IAAI,qBAAqB,IAAM,EAAA;AAC7B,QAAO,OAAA,SAAA;AAAA,iBAEP,OAAO,SAAA,KAAc,QACrB,IAAA,OAAO,cAAc,QACrB,EAAA;AACA,QAAO,OAAA,IAAI,KAAK,SAAS,CAAA;AAAA;AAE3B,MAAA,2BAAW,IAAK,EAAA;AAAA,KAClB;AAEA,IAAI,IAAA,YAAA,mBAAmB,IAAA,IAAA,CAAK,CAAC,CAAA;AAC7B,IAAA,IAAI,KAAQ,GAAA,CAAA;AACZ,IAAA,MAAM,eAAuC,EAAC;AAC9C,IAAA,KAAA,MAAW,OAAO,UAAY,EAAA;AAC5B,MAAM,MAAA,YAAA,GAAe,kBAAmB,CAAA,GAAA,CAAI,aAAa,CAAA;AACzD,MAAA,IAAI,eAAe,YAAc,EAAA;AAC/B,QAAe,YAAA,GAAA,YAAA;AAAA;AAEjB,MAAA,MAAM,OAAO,GAAI,CAAA,MAAA;AACjB,MAAM,MAAA,KAAA,GAAQ,MAAO,CAAA,GAAA,CAAI,KAAK,CAAA;AAC9B,MAAA,YAAA,CAAa,IAAI,CAAI,GAAA,KAAA;AACrB,MAAS,KAAA,IAAA,KAAA;AAAA;AAGX,IAAO,OAAA;AAAA,MACL,SAAA;AAAA,MACA,KAAA;AAAA,MACA,aAAe,EAAA,YAAA;AAAA,MACf;AAAA,KACF;AAAA;AAEJ;;;;"}
|
|
1
|
+
{"version":3,"file":"DatabaseMetricValues.cjs.js","sources":["../../src/database/DatabaseMetricValues.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Knex } from 'knex';\nimport {\n DbMetricValueCreate,\n DbMetricValue,\n DbAggregatedMetric,\n} from './types';\n\ntype ReadEntityMetricsWithFiltersOptions = {\n status?: string;\n entityName?: string;\n entityKind?: string;\n entityNamespace?: string;\n entityOwner?: string[];\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n pagination?: { limit: number; offset: number };\n};\n\nexport class DatabaseMetricValues {\n private readonly tableName = 'metric_values';\n\n constructor(private readonly dbClient: Knex<any, any[]>) {}\n\n /**\n * Insert multiple metric values\n */\n async createMetricValues(metricValues: DbMetricValueCreate[]): Promise<void> {\n if (metricValues.length === 0) {\n return;\n }\n await this.dbClient(this.tableName).insert(metricValues);\n }\n\n /**\n * Get the latest metric values for a specific entity and metrics\n */\n async readLatestEntityMetricValues(\n catalog_entity_ref: string,\n metric_ids: string[],\n ): Promise<DbMetricValue[]> {\n return await this.dbClient(this.tableName)\n .select('*')\n .whereIn(\n 'id',\n this.dbClient(this.tableName)\n .max('id')\n .whereIn('metric_id', metric_ids)\n .where('catalog_entity_ref', catalog_entity_ref)\n .groupBy('metric_id'),\n );\n }\n\n /**\n * Delete metric values that are older than the given date\n */\n async cleanupExpiredMetrics(olderThan: Date): Promise<number> {\n return await this.dbClient(this.tableName)\n .where('timestamp', '<', olderThan)\n .del();\n }\n\n /**\n * Get aggregated metrics by status for multiple entities and metrics.\n */\n async readAggregatedMetricByEntityRefs(\n catalog_entity_refs: string[],\n metric_id: string,\n ): Promise<DbAggregatedMetric | undefined> {\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .whereIn('catalog_entity_ref', catalog_entity_refs)\n .groupBy('catalog_entity_ref');\n\n const statusRows = await this.dbClient(this.tableName)\n .select('status')\n .count('* as count')\n .max('timestamp as max_timestamp')\n .whereIn('id', latestIdsSubquery)\n .whereNotNull('status')\n .whereNotNull('value')\n .groupBy('status');\n\n if (!statusRows || statusRows.length === 0) {\n return undefined;\n }\n\n // Normalize types for cross-database compatibility\n // PostgreSQL returns COUNT/SUM as strings, SQLite returns numbers\n // PostgreSQL returns MAX(timestamp) as Date, SQLite returns number (milliseconds)\n const normalizeTimestamp = (timestamp: any): Date => {\n if (timestamp instanceof Date) {\n return timestamp;\n } else if (\n typeof timestamp === 'number' ||\n typeof timestamp === 'string'\n ) {\n return new Date(timestamp);\n }\n return new Date();\n };\n\n let maxTimestamp = new Date(0);\n let total = 0;\n const statusCounts: Record<string, number> = {};\n for (const row of statusRows) {\n const rowTimestamp = normalizeTimestamp(row.max_timestamp);\n if (rowTimestamp > maxTimestamp) {\n maxTimestamp = rowTimestamp;\n }\n const name = row.status as string;\n const count = Number(row.count);\n statusCounts[name] = count;\n total += count;\n }\n\n return {\n metric_id,\n total,\n max_timestamp: maxTimestamp,\n statusCounts,\n };\n }\n\n /**\n * Fetch the latest entity metric values for a given metric, with optional filtering\n * by status, name, kind, namespace, or owner, plus sorting and pagination.\n */\n async readEntityMetricsWithFilters(\n metric_id: string,\n options: ReadEntityMetricsWithFiltersOptions,\n ): Promise<DbMetricValue[]> {\n const clientName: string =\n (this.dbClient as any).client?.config?.client ?? '';\n const isPostgres = clientName === 'pg' || clientName.includes('postgres');\n\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .groupBy('catalog_entity_ref');\n\n const query = this.dbClient(this.tableName)\n .select('*')\n .whereIn('id', latestIdsSubquery);\n\n const sortColumnMap: Record<string, string> = {\n entityName: 'catalog_entity_ref',\n owner: 'entity_owner',\n entityKind: 'entity_kind',\n timestamp: 'timestamp',\n metricValue: 'value',\n namespace: 'entity_namespace',\n status: 'status',\n };\n\n const column =\n (options.sortBy && sortColumnMap[options.sortBy]) ?? 'timestamp';\n const direction = options.sortOrder === 'asc' ? 'asc' : 'desc';\n\n this.applySort(query, options.sortBy, column, direction, isPostgres);\n\n if (options.status) {\n query.where('status', options.status);\n }\n\n if (options.entityName) {\n const escaped = options.entityName.replace(/[%_\\\\]/g, '\\\\$&');\n query.whereRaw(\"catalog_entity_ref LIKE ? ESCAPE '\\\\'\", [`%${escaped}%`]);\n }\n\n if (options.entityKind) {\n query.where('entity_kind', options.entityKind);\n }\n\n if (options.entityNamespace) {\n query.where('entity_namespace', options.entityNamespace);\n }\n\n if (options.entityOwner && options.entityOwner.length > 0) {\n query.whereIn('entity_owner', options.entityOwner);\n }\n\n if (options.pagination) {\n query.limit(options.pagination.limit).offset(options.pagination.offset);\n }\n\n return await query;\n }\n\n private applySort(\n query: any,\n sortBy: string | undefined,\n column: string,\n direction: string,\n isPostgres: boolean,\n ): void {\n if (sortBy === 'metricValue') {\n // value is JSON and nullable; cast for numeric sort with NULLs last\n if (isPostgres) {\n query.orderByRaw(\n `CAST(value::text AS DOUBLE PRECISION) ${direction} NULLS LAST, id ASC`,\n );\n } else {\n // SQLite: \"value IS NULL\" puts nulls last; double-cast handles JSON-stored values\n query.orderByRaw(\n `value IS NULL, CAST(CAST(value AS TEXT) AS REAL) ${direction}, id ASC`,\n );\n }\n } else if (sortBy === 'status') {\n // status is nullable; NULLs always sort last regardless of direction\n if (isPostgres) {\n query.orderByRaw(`status ${direction} NULLS LAST, id ASC`);\n } else {\n // SQLite: \"status IS NULL\" evaluates to 1 for NULLs, pushing them to the end\n query.orderByRaw(`status IS NULL, status ${direction}, id ASC`);\n }\n } else {\n query.orderBy(column, direction);\n // Ensure a stable sort when two metrics share the same primary sort value\n query.orderBy('id', 'asc');\n }\n }\n}\n"],"names":[],"mappings":";;AAyCO,MAAM,oBAAqB,CAAA;AAAA,EAGhC,YAA6B,QAA4B,EAAA;AAA5B,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA;AAA6B,EAA7B,QAAA;AAAA,EAFZ,SAAY,GAAA,eAAA;AAAA;AAAA;AAAA;AAAA,EAO7B,MAAM,mBAAmB,YAAoD,EAAA;AAC3E,IAAI,IAAA,YAAA,CAAa,WAAW,CAAG,EAAA;AAC7B,MAAA;AAAA;AAEF,IAAA,MAAM,KAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CAAA,CAAE,OAAO,YAAY,CAAA;AAAA;AACzD;AAAA;AAAA;AAAA,EAKA,MAAM,4BACJ,CAAA,kBAAA,EACA,UAC0B,EAAA;AAC1B,IAAO,OAAA,MAAM,KAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CACtC,CAAA,MAAA,CAAO,GAAG,CACV,CAAA,OAAA;AAAA,MACC,IAAA;AAAA,MACA,KAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CAAA,CACzB,IAAI,IAAI,CAAA,CACR,OAAQ,CAAA,WAAA,EAAa,UAAU,CAC/B,CAAA,KAAA,CAAM,sBAAsB,kBAAkB,CAAA,CAC9C,QAAQ,WAAW;AAAA,KACxB;AAAA;AACJ;AAAA;AAAA;AAAA,EAKA,MAAM,sBAAsB,SAAkC,EAAA;AAC5D,IAAO,OAAA,MAAM,IAAK,CAAA,QAAA,CAAS,IAAK,CAAA,SAAS,CACtC,CAAA,KAAA,CAAM,WAAa,EAAA,GAAA,EAAK,SAAS,CAAA,CACjC,GAAI,EAAA;AAAA;AACT;AAAA;AAAA;AAAA,EAKA,MAAM,gCACJ,CAAA,mBAAA,EACA,SACyC,EAAA;AACzC,IAAA,MAAM,oBAAoB,IAAK,CAAA,QAAA,CAAS,KAAK,SAAS,CAAA,CACnD,IAAI,IAAI,CAAA,CACR,KAAM,CAAA,WAAA,EAAa,SAAS,CAC5B,CAAA,OAAA,CAAQ,sBAAsB,mBAAmB,CAAA,CACjD,QAAQ,oBAAoB,CAAA;AAE/B,IAAM,MAAA,UAAA,GAAa,MAAM,IAAA,CAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CAAA,CAClD,MAAO,CAAA,QAAQ,CACf,CAAA,KAAA,CAAM,YAAY,CAAA,CAClB,GAAI,CAAA,4BAA4B,CAChC,CAAA,OAAA,CAAQ,IAAM,EAAA,iBAAiB,CAC/B,CAAA,YAAA,CAAa,QAAQ,CAAA,CACrB,YAAa,CAAA,OAAO,CACpB,CAAA,OAAA,CAAQ,QAAQ,CAAA;AAEnB,IAAA,IAAI,CAAC,UAAA,IAAc,UAAW,CAAA,MAAA,KAAW,CAAG,EAAA;AAC1C,MAAO,OAAA,MAAA;AAAA;AAMT,IAAM,MAAA,kBAAA,GAAqB,CAAC,SAAyB,KAAA;AACnD,MAAA,IAAI,qBAAqB,IAAM,EAAA;AAC7B,QAAO,OAAA,SAAA;AAAA,iBAEP,OAAO,SAAA,KAAc,QACrB,IAAA,OAAO,cAAc,QACrB,EAAA;AACA,QAAO,OAAA,IAAI,KAAK,SAAS,CAAA;AAAA;AAE3B,MAAA,2BAAW,IAAK,EAAA;AAAA,KAClB;AAEA,IAAI,IAAA,YAAA,mBAAmB,IAAA,IAAA,CAAK,CAAC,CAAA;AAC7B,IAAA,IAAI,KAAQ,GAAA,CAAA;AACZ,IAAA,MAAM,eAAuC,EAAC;AAC9C,IAAA,KAAA,MAAW,OAAO,UAAY,EAAA;AAC5B,MAAM,MAAA,YAAA,GAAe,kBAAmB,CAAA,GAAA,CAAI,aAAa,CAAA;AACzD,MAAA,IAAI,eAAe,YAAc,EAAA;AAC/B,QAAe,YAAA,GAAA,YAAA;AAAA;AAEjB,MAAA,MAAM,OAAO,GAAI,CAAA,MAAA;AACjB,MAAM,MAAA,KAAA,GAAQ,MAAO,CAAA,GAAA,CAAI,KAAK,CAAA;AAC9B,MAAA,YAAA,CAAa,IAAI,CAAI,GAAA,KAAA;AACrB,MAAS,KAAA,IAAA,KAAA;AAAA;AAGX,IAAO,OAAA;AAAA,MACL,SAAA;AAAA,MACA,KAAA;AAAA,MACA,aAAe,EAAA,YAAA;AAAA,MACf;AAAA,KACF;AAAA;AACF;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,4BACJ,CAAA,SAAA,EACA,OAC0B,EAAA;AAC1B,IAAA,MAAM,UACH,GAAA,IAAA,CAAK,QAAiB,CAAA,MAAA,EAAQ,QAAQ,MAAU,IAAA,EAAA;AACnD,IAAA,MAAM,UAAa,GAAA,UAAA,KAAe,IAAQ,IAAA,UAAA,CAAW,SAAS,UAAU,CAAA;AAExE,IAAA,MAAM,iBAAoB,GAAA,IAAA,CAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CACnD,CAAA,GAAA,CAAI,IAAI,CAAA,CACR,KAAM,CAAA,WAAA,EAAa,SAAS,CAAA,CAC5B,QAAQ,oBAAoB,CAAA;AAE/B,IAAM,MAAA,KAAA,GAAQ,IAAK,CAAA,QAAA,CAAS,IAAK,CAAA,SAAS,CACvC,CAAA,MAAA,CAAO,GAAG,CAAA,CACV,OAAQ,CAAA,IAAA,EAAM,iBAAiB,CAAA;AAElC,IAAA,MAAM,aAAwC,GAAA;AAAA,MAC5C,UAAY,EAAA,oBAAA;AAAA,MACZ,KAAO,EAAA,cAAA;AAAA,MACP,UAAY,EAAA,aAAA;AAAA,MACZ,SAAW,EAAA,WAAA;AAAA,MACX,WAAa,EAAA,OAAA;AAAA,MACb,SAAW,EAAA,kBAAA;AAAA,MACX,MAAQ,EAAA;AAAA,KACV;AAEA,IAAA,MAAM,UACH,OAAQ,CAAA,MAAA,IAAU,aAAc,CAAA,OAAA,CAAQ,MAAM,CAAM,KAAA,WAAA;AACvD,IAAA,MAAM,SAAY,GAAA,OAAA,CAAQ,SAAc,KAAA,KAAA,GAAQ,KAAQ,GAAA,MAAA;AAExD,IAAA,IAAA,CAAK,UAAU,KAAO,EAAA,OAAA,CAAQ,MAAQ,EAAA,MAAA,EAAQ,WAAW,UAAU,CAAA;AAEnE,IAAA,IAAI,QAAQ,MAAQ,EAAA;AAClB,MAAM,KAAA,CAAA,KAAA,CAAM,QAAU,EAAA,OAAA,CAAQ,MAAM,CAAA;AAAA;AAGtC,IAAA,IAAI,QAAQ,UAAY,EAAA;AACtB,MAAA,MAAM,OAAU,GAAA,OAAA,CAAQ,UAAW,CAAA,OAAA,CAAQ,WAAW,MAAM,CAAA;AAC5D,MAAA,KAAA,CAAM,SAAS,uCAAyC,EAAA,CAAC,CAAI,CAAA,EAAA,OAAO,GAAG,CAAC,CAAA;AAAA;AAG1E,IAAA,IAAI,QAAQ,UAAY,EAAA;AACtB,MAAM,KAAA,CAAA,KAAA,CAAM,aAAe,EAAA,OAAA,CAAQ,UAAU,CAAA;AAAA;AAG/C,IAAA,IAAI,QAAQ,eAAiB,EAAA;AAC3B,MAAM,KAAA,CAAA,KAAA,CAAM,kBAAoB,EAAA,OAAA,CAAQ,eAAe,CAAA;AAAA;AAGzD,IAAA,IAAI,OAAQ,CAAA,WAAA,IAAe,OAAQ,CAAA,WAAA,CAAY,SAAS,CAAG,EAAA;AACzD,MAAM,KAAA,CAAA,OAAA,CAAQ,cAAgB,EAAA,OAAA,CAAQ,WAAW,CAAA;AAAA;AAGnD,IAAA,IAAI,QAAQ,UAAY,EAAA;AACtB,MAAM,KAAA,CAAA,KAAA,CAAM,QAAQ,UAAW,CAAA,KAAK,EAAE,MAAO,CAAA,OAAA,CAAQ,WAAW,MAAM,CAAA;AAAA;AAGxE,IAAA,OAAO,MAAM,KAAA;AAAA;AACf,EAEQ,SACN,CAAA,KAAA,EACA,MACA,EAAA,MAAA,EACA,WACA,UACM,EAAA;AACN,IAAA,IAAI,WAAW,aAAe,EAAA;AAE5B,MAAA,IAAI,UAAY,EAAA;AACd,QAAM,KAAA,CAAA,UAAA;AAAA,UACJ,yCAAyC,SAAS,CAAA,mBAAA;AAAA,SACpD;AAAA,OACK,MAAA;AAEL,QAAM,KAAA,CAAA,UAAA;AAAA,UACJ,oDAAoD,SAAS,CAAA,QAAA;AAAA,SAC/D;AAAA;AACF,KACF,MAAA,IAAW,WAAW,QAAU,EAAA;AAE9B,MAAA,IAAI,UAAY,EAAA;AACd,QAAM,KAAA,CAAA,UAAA,CAAW,CAAU,OAAA,EAAA,SAAS,CAAqB,mBAAA,CAAA,CAAA;AAAA,OACpD,MAAA;AAEL,QAAM,KAAA,CAAA,UAAA,CAAW,CAA0B,uBAAA,EAAA,SAAS,CAAU,QAAA,CAAA,CAAA;AAAA;AAChE,KACK,MAAA;AACL,MAAM,KAAA,CAAA,OAAA,CAAQ,QAAQ,SAAS,CAAA;AAE/B,MAAM,KAAA,CAAA,OAAA,CAAQ,MAAM,KAAK,CAAA;AAAA;AAC3B;AAEJ;;;;"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
var errors = require('@backstage/errors');
|
|
5
|
+
|
|
6
|
+
function validateAggregationIdParam(req, _res, next) {
|
|
7
|
+
const schema = zod.z.object({
|
|
8
|
+
aggregationId: zod.z.string().min(1).max(255)
|
|
9
|
+
});
|
|
10
|
+
const parsed = schema.safeParse(req.params);
|
|
11
|
+
if (!parsed.success) {
|
|
12
|
+
throw new errors.InputError(
|
|
13
|
+
`Invalid aggregationId parameter: ${parsed.error.message}`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
next();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
exports.validateAggregationIdParam = validateAggregationIdParam;
|
|
20
|
+
//# sourceMappingURL=validateAggregationIdParam.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateAggregationIdParam.cjs.js","sources":["../../src/middlewares/validateAggregationIdParam.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { z } from 'zod';\nimport { InputError } from '@backstage/errors';\nimport type { Request, Response, NextFunction } from 'express';\n\nexport function validateAggregationIdParam(\n req: Request,\n _res: Response,\n next: NextFunction,\n): void {\n const schema = z.object({\n aggregationId: z.string().min(1).max(255),\n });\n\n const parsed = schema.safeParse(req.params);\n\n if (!parsed.success) {\n throw new InputError(\n `Invalid aggregationId parameter: ${parsed.error.message}`,\n );\n }\n\n next();\n}\n"],"names":["z","InputError"],"mappings":";;;;;AAoBgB,SAAA,0BAAA,CACd,GACA,EAAA,IAAA,EACA,IACM,EAAA;AACN,EAAM,MAAA,MAAA,GAASA,MAAE,MAAO,CAAA;AAAA,IACtB,aAAA,EAAeA,MAAE,MAAO,EAAA,CAAE,IAAI,CAAC,CAAA,CAAE,IAAI,GAAG;AAAA,GACzC,CAAA;AAED,EAAA,MAAM,MAAS,GAAA,MAAA,CAAO,SAAU,CAAA,GAAA,CAAI,MAAM,CAAA;AAE1C,EAAI,IAAA,CAAC,OAAO,OAAS,EAAA;AACnB,IAAA,MAAM,IAAIC,iBAAA;AAAA,MACR,CAAA,iCAAA,EAAoC,MAAO,CAAA,KAAA,CAAM,OAAO,CAAA;AAAA,KAC1D;AAAA;AAGF,EAAK,IAAA,EAAA;AACP;;;;"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
var errors = require('@backstage/errors');
|
|
5
|
+
|
|
6
|
+
function validateDatasourceQueryParams(req, _res, next) {
|
|
7
|
+
const schema = zod.z.object({
|
|
8
|
+
datasource: zod.z.string().min(1).max(255).optional()
|
|
9
|
+
});
|
|
10
|
+
const parsed = schema.safeParse(req.query);
|
|
11
|
+
if (!parsed.success) {
|
|
12
|
+
throw new errors.InputError(`Invalid query parameters: ${parsed.error.message}`);
|
|
13
|
+
}
|
|
14
|
+
next();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
exports.validateDatasourceQueryParams = validateDatasourceQueryParams;
|
|
18
|
+
//# sourceMappingURL=validateDatasourceQueryParams.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateDatasourceQueryParams.cjs.js","sources":["../../src/middlewares/validateDatasourceQueryParams.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { z } from 'zod';\nimport { InputError } from '@backstage/errors';\nimport type { Request, Response, NextFunction } from 'express';\n\nexport function validateDatasourceQueryParams(\n req: Request,\n _res: Response,\n next: NextFunction,\n): void {\n const schema = z.object({\n datasource: z.string().min(1).max(255).optional(),\n });\n\n const parsed = schema.safeParse(req.query);\n\n if (!parsed.success) {\n throw new InputError(`Invalid query parameters: ${parsed.error.message}`);\n }\n\n next();\n}\n"],"names":["z","InputError"],"mappings":";;;;;AAoBgB,SAAA,6BAAA,CACd,GACA,EAAA,IAAA,EACA,IACM,EAAA;AACN,EAAM,MAAA,MAAA,GAASA,MAAE,MAAO,CAAA;AAAA,IACtB,UAAA,EAAYA,KAAE,CAAA,MAAA,EAAS,CAAA,GAAA,CAAI,CAAC,CAAE,CAAA,GAAA,CAAI,GAAG,CAAA,CAAE,QAAS;AAAA,GACjD,CAAA;AAED,EAAA,MAAM,MAAS,GAAA,MAAA,CAAO,SAAU,CAAA,GAAA,CAAI,KAAK,CAAA;AAEzC,EAAI,IAAA,CAAC,OAAO,OAAS,EAAA;AACnB,IAAA,MAAM,IAAIC,iBAAW,CAAA,CAAA,0BAAA,EAA6B,MAAO,CAAA,KAAA,CAAM,OAAO,CAAE,CAAA,CAAA;AAAA;AAG1E,EAAK,IAAA,EAAA;AACP;;;;"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
var errors = require('@backstage/errors');
|
|
5
|
+
|
|
6
|
+
function validateMetricIdsQueryParams(req, _res, next) {
|
|
7
|
+
const schema = zod.z.object({
|
|
8
|
+
metricIds: zod.z.string().min(1).max(255).optional()
|
|
9
|
+
});
|
|
10
|
+
const parsed = schema.safeParse(req.query);
|
|
11
|
+
if (!parsed.success) {
|
|
12
|
+
throw new errors.InputError(`Invalid query parameters: ${parsed.error.message}`);
|
|
13
|
+
}
|
|
14
|
+
next();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
exports.validateMetricIdsQueryParams = validateMetricIdsQueryParams;
|
|
18
|
+
//# sourceMappingURL=validateMetricIdsQueryParams.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateMetricIdsQueryParams.cjs.js","sources":["../../src/middlewares/validateMetricIdsQueryParams.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { z } from 'zod';\nimport { InputError } from '@backstage/errors';\nimport type { Request, Response, NextFunction } from 'express';\n\nexport function validateMetricIdsQueryParams(\n req: Request,\n _res: Response,\n next: NextFunction,\n): void {\n const schema = z.object({\n metricIds: z.string().min(1).max(255).optional(),\n });\n\n const parsed = schema.safeParse(req.query);\n\n if (!parsed.success) {\n throw new InputError(`Invalid query parameters: ${parsed.error.message}`);\n }\n\n next();\n}\n"],"names":["z","InputError"],"mappings":";;;;;AAoBgB,SAAA,4BAAA,CACd,GACA,EAAA,IAAA,EACA,IACM,EAAA;AACN,EAAM,MAAA,MAAA,GAASA,MAAE,MAAO,CAAA;AAAA,IACtB,SAAA,EAAWA,KAAE,CAAA,MAAA,EAAS,CAAA,GAAA,CAAI,CAAC,CAAE,CAAA,GAAA,CAAI,GAAG,CAAA,CAAE,QAAS;AAAA,GAChD,CAAA;AAED,EAAA,MAAM,MAAS,GAAA,MAAA,CAAO,SAAU,CAAA,GAAA,CAAI,KAAK,CAAA;AAEzC,EAAI,IAAA,CAAC,OAAO,OAAS,EAAA;AACnB,IAAA,MAAM,IAAIC,iBAAW,CAAA,CAAA,0BAAA,EAA6B,MAAO,CAAA,KAAA,CAAM,OAAO,CAAE,CAAA,CAAA;AAAA;AAG1E,EAAK,IAAA,EAAA;AACP;;;;"}
|
|
@@ -5,6 +5,30 @@ var errors = require('@backstage/errors');
|
|
|
5
5
|
var alpha = require('@backstage/plugin-catalog-common/alpha');
|
|
6
6
|
var rules = require('./rules.cjs.js');
|
|
7
7
|
|
|
8
|
+
const getUserEntityRef = async (credentials) => {
|
|
9
|
+
const userEntityRef = credentials?.principal?.userEntityRef;
|
|
10
|
+
if (!userEntityRef) {
|
|
11
|
+
throw new errors.AuthenticationError("User entity reference not found");
|
|
12
|
+
}
|
|
13
|
+
return userEntityRef;
|
|
14
|
+
};
|
|
15
|
+
const authorizeConditional = async (credentials, permissions, permission) => {
|
|
16
|
+
const scorecardPermission = {};
|
|
17
|
+
if (permission.type === "resource") {
|
|
18
|
+
scorecardPermission.decision = (await permissions.authorizeConditional([{ permission }], {
|
|
19
|
+
credentials
|
|
20
|
+
}))[0];
|
|
21
|
+
} else {
|
|
22
|
+
scorecardPermission.decision = (await permissions.authorize([{ permission }], {
|
|
23
|
+
credentials
|
|
24
|
+
}))[0];
|
|
25
|
+
}
|
|
26
|
+
if (scorecardPermission.decision.result === pluginPermissionCommon.AuthorizeResult.DENY) {
|
|
27
|
+
throw new errors.NotAllowedError();
|
|
28
|
+
}
|
|
29
|
+
scorecardPermission.conditions = scorecardPermission.decision.result === pluginPermissionCommon.AuthorizeResult.CONDITIONAL ? scorecardPermission.decision.conditions : void 0;
|
|
30
|
+
return scorecardPermission;
|
|
31
|
+
};
|
|
8
32
|
const checkEntityAccess = async (entityRef, req, permissions, httpAuth) => {
|
|
9
33
|
const entityAccessDecision = await permissions.authorize(
|
|
10
34
|
[{ permission: alpha.catalogEntityReadPermission, resourceRef: entityRef }],
|
|
@@ -36,7 +60,9 @@ const filterAuthorizedMetrics = (metrics, filter) => {
|
|
|
36
60
|
return metrics.filter((metric) => matches(metric, filter));
|
|
37
61
|
};
|
|
38
62
|
|
|
63
|
+
exports.authorizeConditional = authorizeConditional;
|
|
39
64
|
exports.checkEntityAccess = checkEntityAccess;
|
|
40
65
|
exports.filterAuthorizedMetrics = filterAuthorizedMetrics;
|
|
66
|
+
exports.getUserEntityRef = getUserEntityRef;
|
|
41
67
|
exports.matches = matches;
|
|
42
68
|
//# sourceMappingURL=permissionUtils.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissionUtils.cjs.js","sources":["../../src/permissions/permissionUtils.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n PermissionCondition,\n PermissionCriteria,\n PermissionRuleParams,\n
|
|
1
|
+
{"version":3,"file":"permissionUtils.cjs.js","sources":["../../src/permissions/permissionUtils.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthorizeResult,\n BasicPermission,\n PermissionCondition,\n PermissionCriteria,\n PermissionRuleParams,\n PolicyDecision,\n ResourcePermission,\n} from '@backstage/plugin-permission-common';\nimport { Request } from 'express';\nimport { AuthenticationError, NotAllowedError } from '@backstage/errors';\nimport { catalogEntityReadPermission } from '@backstage/plugin-catalog-common/alpha';\nimport type {\n BackstageCredentials,\n BackstageUserPrincipal,\n HttpAuthService,\n PermissionsService,\n} from '@backstage/backend-plugin-api';\n\nimport { Metric } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\n\nimport { rules as scorecardRules } from './rules';\n\nexport const getUserEntityRef = async (\n credentials: BackstageCredentials<BackstageUserPrincipal>,\n) => {\n const userEntityRef = credentials?.principal?.userEntityRef;\n\n if (!userEntityRef) {\n throw new AuthenticationError('User entity reference not found');\n }\n\n return userEntityRef;\n};\n\ntype ScorecardPermission = {\n decision: PolicyDecision;\n conditions?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >;\n};\n\nexport const authorizeConditional = async (\n credentials: BackstageCredentials,\n permissions: PermissionsService,\n permission: ResourcePermission<'scorecard-metric'> | BasicPermission,\n) => {\n const scorecardPermission = {} as ScorecardPermission;\n\n if (permission.type === 'resource') {\n scorecardPermission.decision = (\n await permissions.authorizeConditional([{ permission }], {\n credentials,\n })\n )[0];\n } else {\n scorecardPermission.decision = (\n await permissions.authorize([{ permission }], {\n credentials,\n })\n )[0];\n }\n\n if (scorecardPermission.decision.result === AuthorizeResult.DENY) {\n throw new NotAllowedError();\n }\n\n scorecardPermission.conditions =\n scorecardPermission.decision.result === AuthorizeResult.CONDITIONAL\n ? scorecardPermission.decision.conditions\n : undefined;\n\n return scorecardPermission;\n};\n\nexport const checkEntityAccess = async (\n entityRef: string,\n req: Request,\n permissions: PermissionsService,\n httpAuth: HttpAuthService,\n): Promise<void> => {\n const entityAccessDecision = await permissions.authorize(\n [{ permission: catalogEntityReadPermission, resourceRef: entityRef }],\n { credentials: await httpAuth.credentials(req) },\n );\n\n if (entityAccessDecision[0].result !== AuthorizeResult.ALLOW) {\n throw new NotAllowedError(`Access to \"${entityRef}\" entity metrics denied`);\n }\n};\n\nexport const matches = (\n metric: Metric,\n filters?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >,\n): boolean => {\n if (!filters) {\n return true;\n }\n\n if ('allOf' in filters) {\n return filters.allOf.every(filter => matches(metric, filter));\n }\n\n if ('anyOf' in filters) {\n return filters.anyOf.some(filter => matches(metric, filter));\n }\n\n if ('not' in filters) {\n return !matches(metric, filters.not);\n }\n return (\n Object.values(scorecardRules)\n .find(r => r.name === filters.rule)\n ?.apply(metric, filters.params ?? {}) ?? false\n );\n};\n\nexport const filterAuthorizedMetrics = (\n metrics: Metric[],\n filter?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >,\n) => {\n if (!filter) {\n return metrics;\n }\n\n return metrics.filter(metric => matches(metric, filter));\n};\n"],"names":["AuthenticationError","AuthorizeResult","NotAllowedError","catalogEntityReadPermission","scorecardRules"],"mappings":"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"}
|
|
@@ -18,7 +18,10 @@ const hasMetricId = pluginPermissionNode.createPermissionRule({
|
|
|
18
18
|
apply: (metric, { metricIds }) => {
|
|
19
19
|
return metricIds && metricIds.length > 0 ? metricIds.includes(metric.id) : true;
|
|
20
20
|
},
|
|
21
|
-
toQuery: ({ metricIds }) => ({
|
|
21
|
+
toQuery: ({ metricIds }) => ({
|
|
22
|
+
key: "metricId",
|
|
23
|
+
values: metricIds
|
|
24
|
+
})
|
|
22
25
|
});
|
|
23
26
|
const rules = { hasMetricId };
|
|
24
27
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.cjs.js","sources":["../../src/permissions/rules.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n createPermissionResourceRef,\n createPermissionRule,\n} from '@backstage/plugin-permission-node';\nimport { z } from 'zod';\nimport {\n Metric,\n RESOURCE_TYPE_SCORECARD_METRIC,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\n\nexport type ScorecardFilter = {\n key: string;\n values: Array<string> | undefined;\n};\n\nexport type ScorecardFilters =\n | { anyOf: ScorecardFilters[] }\n | { allOf: ScorecardFilters[] }\n | { not: ScorecardFilters }\n | ScorecardFilter;\n\nexport const scorecardMetricPermissionResourceRef = createPermissionResourceRef<\n Metric,\n ScorecardFilter\n>().with({\n pluginId: 'scorecard',\n resourceType: RESOURCE_TYPE_SCORECARD_METRIC,\n});\n\nconst hasMetricId = createPermissionRule({\n name: 'HAS_METRIC_ID',\n description: 'Should allow users to access metrics with specified metric IDs',\n resourceRef: scorecardMetricPermissionResourceRef,\n\n paramsSchema: z.object({\n metricIds: z\n .string()\n .array()\n .optional()\n .describe('List of metric IDs to match on'),\n }),\n apply: (metric: Metric, { metricIds }) => {\n return metricIds && metricIds.length > 0\n ? metricIds.includes(metric.id)\n : true;\n },\n toQuery: ({ metricIds }) => ({
|
|
1
|
+
{"version":3,"file":"rules.cjs.js","sources":["../../src/permissions/rules.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n createPermissionResourceRef,\n createPermissionRule,\n type PermissionRule,\n} from '@backstage/plugin-permission-node';\nimport { z } from 'zod';\nimport {\n Metric,\n RESOURCE_TYPE_SCORECARD_METRIC,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\n\nexport type ScorecardFilter = {\n key: string;\n values: Array<string> | undefined;\n};\n\nexport type ScorecardFilters =\n | { anyOf: ScorecardFilters[] }\n | { allOf: ScorecardFilters[] }\n | { not: ScorecardFilters }\n | ScorecardFilter;\n\nexport const scorecardMetricPermissionResourceRef = createPermissionResourceRef<\n Metric,\n ScorecardFilter\n>().with({\n pluginId: 'scorecard',\n resourceType: RESOURCE_TYPE_SCORECARD_METRIC,\n});\n\ntype HasMetricIdParams = { metricIds?: string[] | undefined };\n\nconst hasMetricId = createPermissionRule({\n name: 'HAS_METRIC_ID',\n description: 'Should allow users to access metrics with specified metric IDs',\n resourceRef: scorecardMetricPermissionResourceRef,\n\n paramsSchema: z.object({\n metricIds: z\n .string()\n .array()\n .optional()\n .describe('List of metric IDs to match on'),\n }),\n apply: (metric: Metric, { metricIds }: HasMetricIdParams) => {\n return metricIds && metricIds.length > 0\n ? metricIds.includes(metric.id)\n : true;\n },\n toQuery: ({ metricIds }: HasMetricIdParams) => ({\n key: 'metricId',\n values: metricIds,\n }),\n} as any) as unknown as PermissionRule<\n Metric,\n ScorecardFilter,\n typeof RESOURCE_TYPE_SCORECARD_METRIC,\n HasMetricIdParams\n>;\n\nexport const rules = { hasMetricId };\n"],"names":["createPermissionResourceRef","RESOURCE_TYPE_SCORECARD_METRIC","createPermissionRule","z"],"mappings":";;;;;;AAsCa,MAAA,oCAAA,GAAuCA,gDAGlD,EAAA,CAAE,IAAK,CAAA;AAAA,EACP,QAAU,EAAA,WAAA;AAAA,EACV,YAAc,EAAAC;AAChB,CAAC;AAID,MAAM,cAAcC,yCAAqB,CAAA;AAAA,EACvC,IAAM,EAAA,eAAA;AAAA,EACN,WAAa,EAAA,gEAAA;AAAA,EACb,WAAa,EAAA,oCAAA;AAAA,EAEb,YAAA,EAAcC,MAAE,MAAO,CAAA;AAAA,IACrB,SAAA,EAAWA,MACR,MAAO,EAAA,CACP,OACA,CAAA,QAAA,EACA,CAAA,QAAA,CAAS,gCAAgC;AAAA,GAC7C,CAAA;AAAA,EACD,KAAO,EAAA,CAAC,MAAgB,EAAA,EAAE,WAAmC,KAAA;AAC3D,IAAO,OAAA,SAAA,IAAa,UAAU,MAAS,GAAA,CAAA,GACnC,UAAU,QAAS,CAAA,MAAA,CAAO,EAAE,CAC5B,GAAA,IAAA;AAAA,GACN;AAAA,EACA,OAAS,EAAA,CAAC,EAAE,SAAA,EAAoC,MAAA;AAAA,IAC9C,GAAK,EAAA,UAAA;AAAA,IACL,MAAQ,EAAA;AAAA,GACV;AACF,CAAQ,CAAA;AAOK,MAAA,KAAA,GAAQ,EAAE,WAAY;;;;;"}
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -12,6 +12,7 @@ var rules = require('./permissions/rules.cjs.js');
|
|
|
12
12
|
var migration = require('./database/migration.cjs.js');
|
|
13
13
|
var DatabaseMetricValues = require('./database/DatabaseMetricValues.cjs.js');
|
|
14
14
|
var index = require('./scheduler/index.cjs.js');
|
|
15
|
+
var validateAggregationConfig = require('./validation/validateAggregationConfig.cjs.js');
|
|
15
16
|
|
|
16
17
|
const scorecardPlugin = backendPluginApi.createBackendPlugin({
|
|
17
18
|
pluginId: "scorecard",
|
|
@@ -64,7 +65,13 @@ const scorecardPlugin = backendPluginApi.createBackendPlugin({
|
|
|
64
65
|
catalog,
|
|
65
66
|
auth,
|
|
66
67
|
registry: metricProvidersRegistry,
|
|
67
|
-
database: dbMetricValues
|
|
68
|
+
database: dbMetricValues,
|
|
69
|
+
logger,
|
|
70
|
+
config
|
|
71
|
+
});
|
|
72
|
+
validateAggregationConfig.validateAggregationConfig({
|
|
73
|
+
rootConfig: config,
|
|
74
|
+
registry: metricProvidersRegistry
|
|
68
75
|
});
|
|
69
76
|
index.Scheduler.create({
|
|
70
77
|
auth,
|
|
@@ -82,7 +89,8 @@ const scorecardPlugin = backendPluginApi.createBackendPlugin({
|
|
|
82
89
|
catalogMetricService,
|
|
83
90
|
catalog,
|
|
84
91
|
httpAuth,
|
|
85
|
-
permissions
|
|
92
|
+
permissions,
|
|
93
|
+
logger
|
|
86
94
|
})
|
|
87
95
|
);
|
|
88
96
|
}
|