Merge branch 'master' into mobile-sidebar
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/config': patch
|
||||
---
|
||||
|
||||
The `ConfigReader#get` method now always returns a deep clone of the configuration data.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-ldap': patch
|
||||
---
|
||||
|
||||
Make sure to avoid accidental data sharing / mutation of `set` values
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-permission-node': patch
|
||||
---
|
||||
|
||||
Add helpers for creating PermissionRules with inferred types
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-ldap': patch
|
||||
'@backstage/plugin-catalog-backend-module-msgraph': patch
|
||||
---
|
||||
|
||||
Clean up API report
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Add permissions to create-app's PluginEnvironment
|
||||
|
||||
`CatalogEnvironment` now has a `permissions` field, which means that a permission client must now be provided as part of `PluginEnvironment`. To apply these changes to an existing app, add the following to the `makeCreateEnv` function in `packages/backend/src/index.ts`:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.ts
|
||||
|
||||
+ import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
|
||||
function makeCreateEnv(config: Config) {
|
||||
...
|
||||
+ const permissions = ServerPerimssionClient.fromConfig(config, {
|
||||
+ discovery,
|
||||
+ tokenManager,
|
||||
+ });
|
||||
|
||||
root.info(`Created UrlReader ${reader}`);
|
||||
|
||||
return (plugin: string): PluginEnvironment => {
|
||||
...
|
||||
return {
|
||||
logger,
|
||||
cache,
|
||||
database,
|
||||
config,
|
||||
reader,
|
||||
discovery,
|
||||
tokenManager,
|
||||
scheduler,
|
||||
+ permissions,
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And add a permissions field to the `PluginEnvironment` type in `packages/backend/src/types.ts`:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/types.ts
|
||||
|
||||
+ import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
|
||||
|
||||
export type PluginEnvironment = {
|
||||
...
|
||||
+ permissions: PermissionAuthorizer;
|
||||
};
|
||||
```
|
||||
|
||||
[`@backstage/plugin-permission-common`](https://www.npmjs.com/package/@backstage/plugin-permission-common) and [`@backstage/plugin-permission-node`](https://www.npmjs.com/package/@backstage/plugin-permission-node) will need to be installed as dependencies:
|
||||
|
||||
```diff
|
||||
// packages/backend/package.json
|
||||
|
||||
+ "@backstage/plugin-permission-common": "...",
|
||||
+ "@backstage/plugin-permission-node": "...",
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
---
|
||||
|
||||
In order to integrate the permissions system with the refresh endpoint in catalog-backend, a new AuthorizedRefreshService was created as a thin wrapper around the existing refresh service which performs authorization and handles the case when authorization is denied. In order to instantiate AuthorizedRefreshService, a permission client is required, which was added as a new field to `CatalogEnvironment`.
|
||||
|
||||
The new `permissions` field in `CatalogEnvironment` should already receive the permission client from the `PluginEnvrionment`, so there should be no changes required to the catalog backend setup. See [the create-app changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md) for more details.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Clean up API reports
|
||||
@@ -136,3 +136,6 @@ site
|
||||
|
||||
# e2e tests
|
||||
cypress/cypress/*
|
||||
|
||||
# Possible leftover from build:api-reports
|
||||
tsconfig.tmp.json
|
||||
|
||||
+3
-2
@@ -77,5 +77,6 @@
|
||||
| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon), [Gauthier Roebroeck](https://github.com/gauthier-roebroeck-mox) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 |
|
||||
| [Keyloop](https://www.keyloop.com/) | [Andre Wanlin](https://github.com/awanlin) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 |
|
||||
| [Simply Business](https://sbtech.simplybusiness.co.uk/) | [@addersuk](https://github.com/addersuk), [@LightningStairs](https://github.com/LightningStairs), [@punitcse](https://github.com/punitcse), [@moltenice](https://github.com/moltenice) | Central developer portal to access everything a developer needs such as docs, internal service catalog, and the ability to quickly create a new service from a template. Internally developed Backstage plugins allow us to customise the experience to how we work. |
|
||||
| [Overwolf](https://www.overwolf.com) | [@tomwolfgang](https://github.com/tomwolfgang) | Dev portal - software catalog, tech-docs, scaffolding |
|
||||
| [Hotmart](https://www.hotmart.com) | [@fabioviana-hotmart](https://github.com/fabioviana-hotmart) | The main Developers Portal to centralize docs, applications and technical metrics. |
|
||||
| [Overwolf](https://www.overwolf.com) | [@tomwolfgang](https://github.com/tomwolfgang) | Dev portal - software catalog, tech-docs, scaffolding |
|
||||
| [Hotmart](https://www.hotmart.com) | [@fabioviana-hotmart](https://github.com/fabioviana-hotmart) | The main Developers Portal to centralize docs, applications and technical metrics. |
|
||||
| [EF Education First](https://www.ef.com) | [Daan Boerlage](https://github.com/runebaas), [Rafał Nowosielski](https://github.com/rnowosielski) | Our developer portal - primarily used for cataloging and scaffolding with the ambition to expand with more feature adoptions over time |
|
||||
|
||||
@@ -97,3 +97,37 @@ of the `SearchType` component.
|
||||
...
|
||||
</Paper>
|
||||
```
|
||||
|
||||
## How to limit what can be searched in the Software Catalog
|
||||
|
||||
The Software Catalog includes a wealth of information about the components,
|
||||
systems, groups, users, and other aspects of your software ecosystem. However,
|
||||
you may not always want _every_ aspect to appear when a user searches the
|
||||
catalog. Examples include:
|
||||
|
||||
- Entities of kind `Location`, which are often not useful to Backstage users.
|
||||
- Entities of kind `User` or `Group`, if you'd prefer that users and groups be
|
||||
exposed to search in a different way (or not at all).
|
||||
|
||||
It's possible to write your own [Collator](./concepts.md#collators) to control
|
||||
exactly what's available to search, (or a [Decorator](./concepts.md#decorators)
|
||||
to filter things out here and there), but the `DefaultCatalogCollator` that's
|
||||
provided by `@backstage/plugin-catalog-backend` offers some configuration too!
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/search.ts
|
||||
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
collator: DefaultCatalogCollator.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager,
|
||||
+ filter: {
|
||||
+ kind: ['API', 'Component', 'Domain', 'Group', 'System', 'User'],
|
||||
+ },
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
As shown above, you can add a catalog entity filter to narrow down what catalog
|
||||
entities are indexed by the search engine.
|
||||
|
||||
@@ -34,9 +34,7 @@ import { Server } from 'http';
|
||||
import * as winston from 'winston';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AwsS3UrlReader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export class AwsS3UrlReader implements UrlReader {
|
||||
constructor(
|
||||
integration: AwsS3Integration,
|
||||
@@ -59,7 +57,7 @@ export class AwsS3UrlReader implements UrlReader {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export class AzureUrlReader implements UrlReader {
|
||||
constructor(
|
||||
integration: AzureIntegration,
|
||||
@@ -114,12 +112,12 @@ export interface CacheClient {
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type CacheClientOptions = {
|
||||
defaultTtl?: number;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type CacheClientSetOptions = {
|
||||
ttl?: number;
|
||||
};
|
||||
@@ -133,18 +131,17 @@ export class CacheManager {
|
||||
): CacheManager;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type CacheManagerOptions = {
|
||||
logger?: Logger_2;
|
||||
onError?: (err: Error) => void;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export const coloredFormat: winston.Logform.Format;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export interface ContainerRunner {
|
||||
// (undocumented)
|
||||
runContainer(opts: RunContainerOptions): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -157,7 +154,7 @@ export function createDatabaseClient(
|
||||
overrides?: Partial<Knex.Config>,
|
||||
): Knex<any, unknown[]>;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function createRootLogger(
|
||||
options?: winston.LoggerOptions,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
@@ -166,14 +163,14 @@ export function createRootLogger(
|
||||
// @public
|
||||
export function createServiceBuilder(_module: NodeModule): ServiceBuilder;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function createStatusCheckRouter(options: {
|
||||
logger: Logger_2;
|
||||
path?: string;
|
||||
statusCheck?: StatusCheck;
|
||||
}): Promise<express.Router>;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export class DatabaseManager {
|
||||
forPlugin(pluginId: string): PluginDatabaseManager;
|
||||
static fromConfig(
|
||||
@@ -187,7 +184,7 @@ export type DatabaseManagerOptions = {
|
||||
migrations?: PluginDatabaseManager['migrations'];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export class DockerContainerRunner implements ContainerRunner {
|
||||
constructor(options: { dockerClient: Docker });
|
||||
// (undocumented)
|
||||
@@ -205,7 +202,7 @@ export function errorHandler(
|
||||
options?: ErrorHandlerOptions,
|
||||
): ErrorRequestHandler;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type ErrorHandlerOptions = {
|
||||
showStackTraces?: boolean;
|
||||
logger?: Logger_2;
|
||||
@@ -218,13 +215,13 @@ export type FromReadableArrayOptions = Array<{
|
||||
path: string;
|
||||
}>;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function getRootLogger(): winston.Logger;
|
||||
|
||||
// @public
|
||||
export function getVoidLogger(): winston.Logger;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export class Git {
|
||||
// (undocumented)
|
||||
add(options: { dir: string; filepath: string }): Promise<void>;
|
||||
@@ -310,7 +307,7 @@ export class GithubUrlReader implements UrlReader {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export class GitlabUrlReader implements UrlReader {
|
||||
constructor(
|
||||
integration: GitLabIntegration,
|
||||
@@ -398,7 +395,7 @@ export type ReadTreeResponseDirOptions = {
|
||||
targetDir?: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export interface ReadTreeResponseFactory {
|
||||
// (undocumented)
|
||||
fromReadableArray(
|
||||
@@ -448,7 +445,7 @@ export type ReadUrlResponse = {
|
||||
// @public
|
||||
export function requestLoggingHandler(logger?: Logger_2): RequestHandler;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type RequestLoggingHandlerFactory = (
|
||||
logger?: Logger_2,
|
||||
) => RequestHandler;
|
||||
@@ -459,7 +456,7 @@ export function resolvePackagePath(name: string, ...paths: string[]): string;
|
||||
// @public
|
||||
export function resolveSafeChildPath(base: string, path: string): string;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type RunContainerOptions = {
|
||||
imageName: string;
|
||||
command?: string | string[];
|
||||
@@ -508,7 +505,7 @@ export class ServerTokenManager implements TokenManager {
|
||||
static noop(): TokenManager;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type ServiceBuilder = {
|
||||
loadConfig(config: Config): ServiceBuilder;
|
||||
setPort(port: number): ServiceBuilder;
|
||||
@@ -534,7 +531,7 @@ export type ServiceBuilder = {
|
||||
start(): Promise<Server>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function setRootLogger(newLogger: winston.Logger): void;
|
||||
|
||||
// @public @deprecated
|
||||
@@ -554,7 +551,7 @@ export class SingleHostDiscovery implements PluginEndpointDiscovery {
|
||||
getExternalBaseUrl(pluginId: string): Promise<string>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type StatusCheck = () => Promise<any>;
|
||||
|
||||
// @public
|
||||
@@ -562,7 +559,7 @@ export function statusCheckHandler(
|
||||
options?: StatusCheckHandlerOptions,
|
||||
): Promise<RequestHandler>;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export interface StatusCheckHandlerOptions {
|
||||
statusCheck?: StatusCheck;
|
||||
}
|
||||
@@ -597,7 +594,7 @@ export class UrlReaders {
|
||||
static default(options: UrlReadersOptions): UrlReader;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type UrlReadersOptions = {
|
||||
config: Config;
|
||||
logger: Logger_2;
|
||||
@@ -612,8 +609,4 @@ export function useHotCleanup(
|
||||
|
||||
// @public
|
||||
export function useHotMemoize<T>(_module: NodeModule, valueFactory: () => T): T;
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/database/types.d.ts:23:12 - (tsdoc-undefined-tag) The TSDoc tag "@default" is not defined in this configuration
|
||||
```
|
||||
|
||||
+5
-1
@@ -22,7 +22,11 @@ type CacheClientArgs = {
|
||||
client: Keyv;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Options passed to {@link CacheClient.set}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type CacheClientSetOptions = {
|
||||
/**
|
||||
* Optional TTL in milliseconds. Defaults to the TTL provided when the client
|
||||
|
||||
+4
-3
@@ -55,8 +55,8 @@ export class CacheManager {
|
||||
private readonly errorHandler: CacheManagerOptions['onError'];
|
||||
|
||||
/**
|
||||
* Creates a new CacheManager instance by reading from the `backend` config
|
||||
* section, specifically the `.cache` key.
|
||||
* Creates a new {@link CacheManager} instance by reading from the `backend`
|
||||
* config section, specifically the `.cache` key.
|
||||
*
|
||||
* @param config - The loaded application configuration.
|
||||
*/
|
||||
@@ -93,7 +93,8 @@ export class CacheManager {
|
||||
/**
|
||||
* Generates a PluginCacheManager for consumption by plugins.
|
||||
*
|
||||
* @param pluginId - The plugin that the cache manager should be created for. Plugin names should be unique.
|
||||
* @param pluginId - The plugin that the cache manager should be created for.
|
||||
* Plugin names should be unique.
|
||||
*/
|
||||
forPlugin(pluginId: string): PluginCacheManager {
|
||||
return {
|
||||
|
||||
+17
-7
@@ -17,7 +17,11 @@
|
||||
import { Logger } from 'winston';
|
||||
import { CacheClient } from './CacheClient';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Options given when constructing a {@link CacheClient}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type CacheClientOptions = {
|
||||
/**
|
||||
* An optional default TTL (in milliseconds) to be set when getting a client
|
||||
@@ -27,7 +31,11 @@ export type CacheClientOptions = {
|
||||
defaultTtl?: number;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Options given when constructing a {@link CacheManager}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type CacheManagerOptions = {
|
||||
/**
|
||||
* An optional logger for use by the PluginCacheManager.
|
||||
@@ -42,17 +50,19 @@ export type CacheManagerOptions = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The PluginCacheManager manages access to cache stores that Plugins get.
|
||||
* Manages access to cache stores that plugins get.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PluginCacheManager = {
|
||||
/**
|
||||
* getClient provides backend plugins cache connections for itself.
|
||||
* Provides backend plugins cache connections for themselves.
|
||||
*
|
||||
* The purpose of this method is to allow plugins to get isolated data
|
||||
* stores so that plugins are discouraged from cache-level integration
|
||||
* and/or cache key collisions.
|
||||
* @remarks
|
||||
*
|
||||
* The purpose of this method is to allow plugins to get isolated data stores
|
||||
* so that plugins are discouraged from cache-level integration and/or cache
|
||||
* key collisions.
|
||||
*/
|
||||
getClient: (options?: CacheClientOptions) => CacheClient;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ function pluginPath(pluginId: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options object.
|
||||
* Creation options for {@link DatabaseManager}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -46,15 +46,20 @@ export type DatabaseManagerOptions = {
|
||||
migrations?: PluginDatabaseManager['migrations'];
|
||||
};
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Manages database connections for Backstage backend plugins.
|
||||
*
|
||||
* The database manager allows the user to set connection and client settings on
|
||||
* a per pluginId basis by defining a database config block under
|
||||
* `plugin.<pluginId>` in addition to top level defaults. Optionally, a user may
|
||||
* set `prefix` which is used to prefix generated database names if config is
|
||||
* not provided.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class DatabaseManager {
|
||||
/**
|
||||
* Creates a DatabaseManager from `backend.database` config.
|
||||
*
|
||||
* The database manager allows the user to set connection and client settings on a per pluginId
|
||||
* basis by defining a database config block under `plugin.<pluginId>` in addition to top level
|
||||
* defaults. Optionally, a user may set `prefix` which is used to prefix generated database
|
||||
* names if config is not provided.
|
||||
* Creates a {@link DatabaseManager} from `backend.database` config.
|
||||
*
|
||||
* @param config - The loaded application configuration.
|
||||
* @param options - An optional configuration object.
|
||||
@@ -108,7 +113,7 @@ export class DatabaseManager {
|
||||
* which is the pluginId prefixed with 'backstage_plugin_'. If `pluginDivisionMode` is
|
||||
* `schema`, it will fallback to using the default database for the knex instance.
|
||||
*
|
||||
* @param pluginId Lookup the database name for given plugin
|
||||
* @param pluginId - Lookup the database name for given plugin
|
||||
* @returns String representing the plugin's database name
|
||||
*/
|
||||
private getDatabaseName(pluginId: string): string | undefined {
|
||||
@@ -143,12 +148,13 @@ export class DatabaseManager {
|
||||
/**
|
||||
* Provides the client type which should be used for a given plugin.
|
||||
*
|
||||
* The client type is determined by plugin specific config if present. Otherwise the base
|
||||
* client is used as the fallback.
|
||||
* The client type is determined by plugin specific config if present.
|
||||
* Otherwise the base client is used as the fallback.
|
||||
*
|
||||
* @param pluginId Plugin to get the client type for
|
||||
* @returns Object with client type returned as `client` and boolean representing whether
|
||||
* or not the client was overridden as `overridden`
|
||||
* @param pluginId - Plugin to get the client type for
|
||||
* @returns Object with client type returned as `client` and boolean
|
||||
* representing whether or not the client was overridden as
|
||||
* `overridden`
|
||||
*/
|
||||
private getClientType(pluginId: string): {
|
||||
client: string;
|
||||
@@ -169,8 +175,8 @@ export class DatabaseManager {
|
||||
/**
|
||||
* Provides the knexConfig which should be used for a given plugin.
|
||||
*
|
||||
* @param pluginId Plugin to get the knexConfig for
|
||||
* @returns the merged kexConfig value or undefined if it isn't specified
|
||||
* @param pluginId - Plugin to get the knexConfig for
|
||||
* @returns The merged knexConfig value or undefined if it isn't specified
|
||||
*/
|
||||
private getAdditionalKnexConfig(pluginId: string): JsonObject | undefined {
|
||||
const pluginConfig = this.config
|
||||
@@ -197,13 +203,15 @@ export class DatabaseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a Knex connection plugin config by combining base and plugin config.
|
||||
* Provides a Knex connection plugin config by combining base and plugin
|
||||
* config.
|
||||
*
|
||||
* This method provides a baseConfig for a plugin database connector. If the client type
|
||||
* has not been overridden, the global connection config will be included with plugin
|
||||
* specific config as the base. Values from the plugin connection take precedence over the
|
||||
* base. Base database name is omitted for all supported databases excluding SQLite unless
|
||||
* `pluginDivisionMode` is set to `schema`.
|
||||
* This method provides a baseConfig for a plugin database connector. If the
|
||||
* client type has not been overridden, the global connection config will be
|
||||
* included with plugin specific config as the base. Values from the plugin
|
||||
* connection take precedence over the base. Base database name is omitted for
|
||||
* all supported databases excluding SQLite unless `pluginDivisionMode` is set
|
||||
* to `schema`.
|
||||
*/
|
||||
private getConnectionConfig(
|
||||
pluginId: string,
|
||||
@@ -249,9 +257,10 @@ export class DatabaseManager {
|
||||
/**
|
||||
* Provides a Knex database config for a given plugin.
|
||||
*
|
||||
* This method provides a Knex configuration object along with the plugin's client type.
|
||||
* This method provides a Knex configuration object along with the plugin's
|
||||
* client type.
|
||||
*
|
||||
* @param pluginId The plugin that the database config should correspond with
|
||||
* @param pluginId - The plugin that the database config should correspond with
|
||||
*/
|
||||
private getConfigForPlugin(pluginId: string): Knex.Config {
|
||||
const { client } = this.getClientType(pluginId);
|
||||
@@ -264,20 +273,21 @@ export class DatabaseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a partial Knex.Config database schema override for a given plugin.
|
||||
* Provides a partial `Knex.Config` database schema override for a given
|
||||
* plugin.
|
||||
*
|
||||
* @param pluginId Target plugin to get database schema override
|
||||
* @returns Partial Knex.Config with database schema override
|
||||
* @param pluginId - Target plugin to get database schema override
|
||||
* @returns Partial `Knex.Config` with database schema override
|
||||
*/
|
||||
private getSchemaOverrides(pluginId: string): Knex.Config | undefined {
|
||||
return createSchemaOverride(this.getClientType(pluginId).client, pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a partial Knex.Config database name override for a given plugin.
|
||||
* Provides a partial `Knex.Config`• database name override for a given plugin.
|
||||
*
|
||||
* @param pluginId Target plugin to get database name override
|
||||
* @returns Partial Knex.Config with database name override
|
||||
* @param pluginId - Target plugin to get database name override
|
||||
* @returns Partial `Knex.Config` with database name override
|
||||
*/
|
||||
private getDatabaseOverrides(pluginId: string): Knex.Config {
|
||||
const databaseName = this.getDatabaseName(pluginId);
|
||||
@@ -289,8 +299,9 @@ export class DatabaseManager {
|
||||
/**
|
||||
* Provides a scoped Knex client for a plugin as per application config.
|
||||
*
|
||||
* @param pluginId Plugin to get a Knex client for
|
||||
* @returns Promise which resolves to a scoped Knex database client for a plugin
|
||||
* @param pluginId - Plugin to get a Knex client for
|
||||
* @returns Promise which resolves to a scoped Knex database client for a
|
||||
* plugin
|
||||
*/
|
||||
private async getDatabase(pluginId: string): Promise<Knex> {
|
||||
const pluginConfig = new ConfigReader(
|
||||
|
||||
@@ -58,7 +58,7 @@ export function createDatabaseClient(
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for createDatabaseClient
|
||||
* Alias for {@link createDatabaseClient}
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use createDatabaseClient instead
|
||||
@@ -100,7 +100,8 @@ export async function ensureSchemaExists(
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a Knex.Config object with the provided database name for a given client.
|
||||
* Provides a `Knex.Config` object with the provided database name for a given
|
||||
* client.
|
||||
*/
|
||||
export function createNameOverride(
|
||||
client: string,
|
||||
@@ -117,7 +118,8 @@ export function createNameOverride(
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a Knex.Config object with the provided database schema for a given client. Currently only supported by `pg`.
|
||||
* Provides a `Knex.Config` object with the provided database schema for a given
|
||||
* client. Currently only supported by `pg`.
|
||||
*/
|
||||
export function createSchemaOverride(
|
||||
client: string,
|
||||
@@ -156,7 +158,8 @@ export function parseConnectionString(
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a connection config or string into an object which can be passed to Knex.
|
||||
* Normalizes a connection config or string into an object which can be passed
|
||||
* to Knex.
|
||||
*/
|
||||
export function normalizeConnection(
|
||||
connection: Knex.StaticConnectionConfig | JsonObject | string | undefined,
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface PluginDatabaseManager {
|
||||
/**
|
||||
* skip database migrations. Useful if connecting to a read-only database.
|
||||
*
|
||||
* @default false
|
||||
* @defaultValue false
|
||||
*/
|
||||
skip?: boolean;
|
||||
};
|
||||
|
||||
@@ -31,7 +31,11 @@ const coloredTemplate = (info: TransformableInfo) => {
|
||||
return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* A logging format that adds coloring to console output.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const coloredFormat = winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.colorize({
|
||||
|
||||
@@ -23,12 +23,29 @@ import { escapeRegExp } from '../util/escapeRegExp';
|
||||
let rootLogger: winston.Logger;
|
||||
let redactionRegExp: RegExp | undefined;
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Gets the current root logger.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getRootLogger(): winston.Logger {
|
||||
return rootLogger;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Sets a completely custom default "root" logger.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This is the logger instance that will be the foundation for all other logger
|
||||
* instances passed to plugins etc, in a given backend.
|
||||
*
|
||||
* Only use this if you absolutely need to make a completely custom logger.
|
||||
* Normally if you want to make light adaptations to the default logger
|
||||
* behavior, you would instead call {@link createRootLogger}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function setRootLogger(newLogger: winston.Logger) {
|
||||
rootLogger = newLogger;
|
||||
}
|
||||
@@ -67,7 +84,17 @@ function redactLogLine(info: winston.Logform.TransformableInfo) {
|
||||
return info;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Creates a default "root" logger. This also calls {@link setRootLogger} under
|
||||
* the hood.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This is the logger instance that will be the foundation for all other logger
|
||||
* instances passed to plugins etc, in a given backend.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createRootLogger(
|
||||
options: winston.LoggerOptions = {},
|
||||
env = process.env,
|
||||
|
||||
@@ -28,7 +28,11 @@ import { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { getRootLogger } from '../logging';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Options passed to the {@link errorHandler} middleware.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ErrorHandlerOptions = {
|
||||
/**
|
||||
* Whether error response bodies should show error stack traces or not.
|
||||
|
||||
@@ -16,10 +16,19 @@
|
||||
|
||||
import { NextFunction, Request, Response, RequestHandler } from 'express';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* A custom status checking function, passed to {@link statusCheckHandler} and
|
||||
* {@link createStatusCheckRouter}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type StatusCheck = () => Promise<any>;
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Options passed to {@link statusCheckHandler}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface StatusCheckHandlerOptions {
|
||||
/**
|
||||
* Optional status function which returns a message.
|
||||
|
||||
@@ -94,6 +94,11 @@ const parseURL = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a {@link UrlReader} for AWS S3 buckets.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class AwsS3UrlReader implements UrlReader {
|
||||
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
|
||||
@@ -38,7 +38,11 @@ import {
|
||||
ReadUrlResponse,
|
||||
} from './types';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Implements a {@link UrlReader} for Azure repos.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class AzureUrlReader implements UrlReader {
|
||||
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
|
||||
@@ -41,8 +41,8 @@ import {
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* A processor that adds the ability to read files from Bitbucket v1 and v2 APIs, such as
|
||||
* the one exposed by Bitbucket Cloud itself.
|
||||
* Implements a {@link UrlReader} for files from Bitbucket v1 and v2 APIs, such
|
||||
* as the one exposed by Bitbucket Cloud itself.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* A UrlReader that does a plain fetch of the URL.
|
||||
* A {@link UrlReader} that does a plain fetch of the URL.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -50,7 +50,7 @@ export type GhBlobResponse =
|
||||
RestEndpointMethodTypes['git']['getBlob']['response']['data'];
|
||||
|
||||
/**
|
||||
* A processor that adds the ability to read files from GitHub v3 APIs, such as
|
||||
* Implements a {@link UrlReader} for files through the GitHub v3 APIs, such as
|
||||
* the one exposed by GitHub itself.
|
||||
*
|
||||
* @public
|
||||
|
||||
@@ -39,7 +39,11 @@ import {
|
||||
} from './types';
|
||||
import { trimEnd } from 'lodash';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Implements a {@link UrlReader} for files on GitLab.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class GitlabUrlReader implements UrlReader {
|
||||
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
|
||||
@@ -48,7 +48,11 @@ const parseURL = (
|
||||
};
|
||||
};
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Implements a {@link UrlReader} for files on Google GCS.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class GoogleGcsUrlReader implements UrlReader {
|
||||
static factory: ReaderFactory = ({ config, logger }) => {
|
||||
if (!config.has('integrations.googleGcs')) {
|
||||
|
||||
@@ -27,7 +27,11 @@ import { FetchUrlReader } from './FetchUrlReader';
|
||||
import { GoogleGcsUrlReader } from './GoogleGcsUrlReader';
|
||||
import { AwsS3UrlReader } from './AwsS3UrlReader';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Creation options for {@link UrlReaders}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type UrlReadersOptions = {
|
||||
/** Root config object */
|
||||
config: Config;
|
||||
@@ -38,13 +42,13 @@ export type UrlReadersOptions = {
|
||||
};
|
||||
|
||||
/**
|
||||
* UrlReaders provide various utilities related to the UrlReader interface.
|
||||
* Helps construct {@link UrlReader}s.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class UrlReaders {
|
||||
/**
|
||||
* Creates a UrlReader without any known types.
|
||||
* Creates a custom {@link UrlReader} wrapper for your own set of factories.
|
||||
*/
|
||||
static create(options: UrlReadersOptions): UrlReader {
|
||||
const { logger, config, factories } = options;
|
||||
@@ -65,7 +69,8 @@ export class UrlReaders {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a UrlReader that includes all the default factories from this package.
|
||||
* Creates a {@link UrlReader} wrapper that includes all the default factories
|
||||
* from this package.
|
||||
*
|
||||
* Any additional factories passed will be loaded before the default ones.
|
||||
*/
|
||||
|
||||
@@ -281,7 +281,12 @@ export type FromReadableArrayOptions = Array<{
|
||||
path: string;
|
||||
}>;
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* A factory for response factories that handle the unpacking and inspection of
|
||||
* complex responses such as archive data.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ReadTreeResponseFactory {
|
||||
fromTarArchive(
|
||||
options: ReadTreeResponseFactoryOptions,
|
||||
|
||||
@@ -33,7 +33,11 @@ From : https://isomorphic-git.org/docs/en/onAuth with fix for GitHub
|
||||
Azure 'notempty' token
|
||||
*/
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* A convenience wrapper around the `isomorphic-git` library.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class Git {
|
||||
private constructor(
|
||||
private readonly config: {
|
||||
|
||||
@@ -19,9 +19,26 @@ import Router from 'express-promise-router';
|
||||
import express from 'express';
|
||||
import { errorHandler, statusCheckHandler, StatusCheck } from '../middleware';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Creates a default status checking router, that you can add to your express
|
||||
* app.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This adds a `/healthcheck` route (or any other path, if given as an
|
||||
* argument), which your infra can call to see if the service is ready to serve
|
||||
* requests.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export async function createStatusCheckRouter(options: {
|
||||
logger: Logger;
|
||||
/**
|
||||
* The path (including a leading slash) that the health check should be
|
||||
* mounted on.
|
||||
*
|
||||
* @defaultValue '/healthcheck'
|
||||
*/
|
||||
path?: string;
|
||||
/**
|
||||
* If not implemented, the default express middleware always returns 200.
|
||||
|
||||
@@ -20,7 +20,11 @@ import { Router, RequestHandler, ErrorRequestHandler } from 'express';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* A helper for building backend service instances.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ServiceBuilder = {
|
||||
/**
|
||||
* Sets the service parameters based on configuration.
|
||||
@@ -119,5 +123,9 @@ export type ServiceBuilder = {
|
||||
start(): Promise<Server>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* A factory for request loggers.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RequestLoggingHandlerFactory = (logger?: Logger) => RequestHandler;
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
|
||||
import { Writable } from 'stream';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Options passed to the {@link ContainerRunner.runContainer} method.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RunContainerOptions = {
|
||||
imageName: string;
|
||||
command?: string | string[];
|
||||
@@ -28,7 +32,14 @@ export type RunContainerOptions = {
|
||||
pullImage?: boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Handles the running of containers, on behalf of others.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ContainerRunner {
|
||||
/**
|
||||
* Runs a container image to completion.
|
||||
*/
|
||||
runContainer(opts: RunContainerOptions): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,11 @@ export type UserOptions = {
|
||||
User?: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* A {@link ContainerRunner} for Docker containers.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class DockerContainerRunner implements ContainerRunner {
|
||||
private readonly dockerClient: Docker;
|
||||
|
||||
|
||||
@@ -265,6 +265,19 @@ describe('ConfigReader', () => {
|
||||
withLogCollector(() => config.getOptionalConfigArray('b')),
|
||||
).toMatchObject({ warn: [] });
|
||||
});
|
||||
|
||||
it('should coerce number strings to numbers', () => {
|
||||
const config = ConfigReader.fromConfigs([
|
||||
{
|
||||
data: {
|
||||
port: '123',
|
||||
},
|
||||
context: '1',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(config.getNumber('port')).toEqual(123);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfigReader with fallback', () => {
|
||||
@@ -661,16 +674,50 @@ describe('ConfigReader.get()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('coerces number strings to numbers', () => {
|
||||
const config = ConfigReader.fromConfigs([
|
||||
{
|
||||
data: {
|
||||
port: '123',
|
||||
},
|
||||
context: '1',
|
||||
it('should return deep clones of the backing data', () => {
|
||||
const data1 = {
|
||||
foo: {
|
||||
bar: [],
|
||||
baz: {},
|
||||
},
|
||||
};
|
||||
const data2 = {
|
||||
x: {
|
||||
y: {
|
||||
z: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const reader = ConfigReader.fromConfigs([
|
||||
{ data: data1, context: '1' },
|
||||
{ data: data2, context: '2' },
|
||||
]);
|
||||
|
||||
expect(config.getNumber('port')).toEqual(123);
|
||||
reader.get<any>().foo.bar.push(1);
|
||||
reader.get<any>('foo').bar.push(1);
|
||||
reader.get<any>('foo.bar').push(1);
|
||||
reader.get<any>().foo.baz.x = 1;
|
||||
reader.get<any>('foo').baz.x = 1;
|
||||
reader.get<any>('foo.baz').x = 1;
|
||||
reader.get<any>().x.y.z.w = 1;
|
||||
reader.get<any>('x').y.z.w = 1;
|
||||
reader.get<any>('x.y').z.w = 1;
|
||||
reader.get<any>('x.y.z').w = 1;
|
||||
|
||||
const readerSingle = ConfigReader.fromConfigs([
|
||||
{ data: data1, context: '1' },
|
||||
]);
|
||||
|
||||
readerSingle.get<any>().foo.bar.push(1);
|
||||
readerSingle.get<any>('foo').bar.push(1);
|
||||
readerSingle.get<any>('foo.bar').push(1);
|
||||
readerSingle.get<any>().foo.baz.x = 1;
|
||||
readerSingle.get<any>('foo').baz.x = 1;
|
||||
readerSingle.get<any>('foo.baz').x = 1;
|
||||
|
||||
expect(data1.foo.bar).toEqual([]);
|
||||
expect(data1.foo.baz).toEqual({});
|
||||
expect(data2.x.y.z).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,7 +126,7 @@ export class ConfigReader implements Config {
|
||||
|
||||
/** {@inheritdoc Config.getOptional} */
|
||||
getOptional<T = JsonValue>(key?: string): T | undefined {
|
||||
const value = this.readValue(key);
|
||||
const value = cloneDeep(this.readValue(key));
|
||||
const fallbackValue = this.fallback?.getOptional<T>(key);
|
||||
|
||||
if (value === undefined) {
|
||||
@@ -153,11 +153,8 @@ export class ConfigReader implements Config {
|
||||
|
||||
// Avoid merging arrays and primitive values, since that's how merging works for other
|
||||
// methods for reading config.
|
||||
return mergeWith(
|
||||
{},
|
||||
{ value: cloneDeep(fallbackValue) },
|
||||
{ value },
|
||||
(into, from) => (!isObject(from) || !isObject(into) ? from : undefined),
|
||||
return mergeWith({}, { value: fallbackValue }, { value }, (into, from) =>
|
||||
!isObject(from) || !isObject(into) ? from : undefined,
|
||||
).value as T;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@
|
||||
"@backstage/plugin-explore": "*",
|
||||
"@backstage/plugin-github-actions": "*",
|
||||
"@backstage/plugin-lighthouse": "*",
|
||||
"@backstage/plugin-permission-common": "*",
|
||||
"@backstage/plugin-permission-node": "*",
|
||||
"@backstage/plugin-proxy-backend": "*",
|
||||
"@backstage/plugin-rollbar-backend": "*",
|
||||
"@backstage/plugin-scaffolder": "*",
|
||||
|
||||
@@ -57,6 +57,8 @@ import { version as pluginExplore } from '../../../../plugins/explore/package.js
|
||||
import { version as pluginGithubActions } from '../../../../plugins/github-actions/package.json';
|
||||
import { version as pluginLighthouse } from '../../../../plugins/lighthouse/package.json';
|
||||
import { version as pluginOrg } from '../../../../plugins/org/package.json';
|
||||
import { version as pluginPermissionCommon } from '../../../../plugins/permission-common/package.json';
|
||||
import { version as pluginPermissionNode } from '../../../../plugins/permission-node/package.json';
|
||||
import { version as pluginProxyBackend } from '../../../../plugins/proxy-backend/package.json';
|
||||
import { version as pluginRollbarBackend } from '../../../../plugins/rollbar-backend/package.json';
|
||||
import { version as pluginScaffolder } from '../../../../plugins/scaffolder/package.json';
|
||||
@@ -94,6 +96,8 @@ export const packageVersions = {
|
||||
'@backstage/plugin-github-actions': pluginGithubActions,
|
||||
'@backstage/plugin-lighthouse': pluginLighthouse,
|
||||
'@backstage/plugin-org': pluginOrg,
|
||||
'@backstage/plugin-permission-common': pluginPermissionCommon,
|
||||
'@backstage/plugin-permission-node': pluginPermissionNode,
|
||||
'@backstage/plugin-proxy-backend': pluginProxyBackend,
|
||||
'@backstage/plugin-rollbar-backend': pluginRollbarBackend,
|
||||
'@backstage/plugin-scaffolder': pluginScaffolder,
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"@backstage/plugin-app-backend": "^{{version '@backstage/plugin-app-backend'}}",
|
||||
"@backstage/plugin-auth-backend": "^{{version '@backstage/plugin-auth-backend'}}",
|
||||
"@backstage/plugin-catalog-backend": "^{{version '@backstage/plugin-catalog-backend'}}",
|
||||
"@backstage/plugin-permission-common": "^{{version '@backstage/plugin-permission-common'}}",
|
||||
"@backstage/plugin-permission-node": "^{{version '@backstage/plugin-permission-node'}}",
|
||||
"@backstage/plugin-proxy-backend": "^{{version '@backstage/plugin-proxy-backend'}}",
|
||||
"@backstage/plugin-scaffolder-backend": "^{{version '@backstage/plugin-scaffolder-backend'}}",
|
||||
"@backstage/plugin-search-backend": "^{{version '@backstage/plugin-search-backend'}}",
|
||||
|
||||
@@ -29,6 +29,7 @@ import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import search from './plugins/search';
|
||||
import { PluginEnvironment } from './types';
|
||||
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
|
||||
function makeCreateEnv(config: Config) {
|
||||
const root = getRootLogger();
|
||||
@@ -38,6 +39,10 @@ function makeCreateEnv(config: Config) {
|
||||
const databaseManager = DatabaseManager.fromConfig(config);
|
||||
const tokenManager = ServerTokenManager.noop();
|
||||
const taskScheduler = TaskScheduler.fromConfig(config);
|
||||
const permissions = ServerPermissionClient.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager,
|
||||
});
|
||||
|
||||
root.info(`Created UrlReader ${reader}`);
|
||||
|
||||
@@ -55,6 +60,7 @@ function makeCreateEnv(config: Config) {
|
||||
discovery,
|
||||
tokenManager,
|
||||
scheduler,
|
||||
permissions,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
UrlReader,
|
||||
} from '@backstage/backend-common';
|
||||
import { PluginTaskScheduler } from '@backstage/backend-tasks';
|
||||
import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
|
||||
|
||||
export type PluginEnvironment = {
|
||||
logger: Logger;
|
||||
@@ -18,4 +19,5 @@ export type PluginEnvironment = {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
tokenManager: TokenManager;
|
||||
scheduler: PluginTaskScheduler;
|
||||
permissions: PermissionAuthorizer;
|
||||
};
|
||||
|
||||
@@ -17,26 +17,26 @@ import { SearchEntry } from 'ldapjs';
|
||||
import { SearchOptions } from 'ldapjs';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "defaultGroupTransformer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type BindConfig = {
|
||||
dn: string;
|
||||
secret: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function defaultGroupTransformer(
|
||||
vendor: LdapVendor,
|
||||
config: GroupConfig,
|
||||
entry: SearchEntry,
|
||||
): Promise<GroupEntity | undefined>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "defaultUserTransformer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function defaultUserTransformer(
|
||||
vendor: LdapVendor,
|
||||
config: UserConfig,
|
||||
entry: SearchEntry,
|
||||
): Promise<UserEntity | undefined>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "GroupConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type GroupConfig = {
|
||||
dn: string;
|
||||
@@ -57,12 +57,6 @@ export type GroupConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-undefined-tag) The TSDoc tag "@return" is not defined in this configuration
|
||||
// Warning: (ae-missing-release-tag) "GroupTransformer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type GroupTransformer = (
|
||||
vendor: LdapVendor,
|
||||
@@ -70,28 +64,18 @@ export type GroupTransformer = (
|
||||
group: SearchEntry,
|
||||
) => Promise<GroupEntity | undefined>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LDAP_DN_ANNOTATION" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const LDAP_DN_ANNOTATION = 'backstage.io/ldap-dn';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LDAP_RDN_ANNOTATION" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const LDAP_RDN_ANNOTATION = 'backstage.io/ldap-rdn';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LDAP_UUID_ANNOTATION" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const LDAP_UUID_ANNOTATION = 'backstage.io/ldap-uuid';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LdapClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export class LdapClient {
|
||||
constructor(client: Client, logger: Logger_2);
|
||||
// Warning: (ae-forgotten-export) The symbol "BindConfig" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
static create(
|
||||
logger: Logger_2,
|
||||
@@ -100,22 +84,14 @@ export class LdapClient {
|
||||
): Promise<LdapClient>;
|
||||
getRootDSE(): Promise<SearchEntry | undefined>;
|
||||
getVendor(): Promise<LdapVendor>;
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
search(dn: string, options: SearchOptions): Promise<SearchEntry[]>;
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (ae-forgotten-export) The symbol "SearchCallback" needs to be exported by the entry point index.d.ts
|
||||
searchStreaming(
|
||||
dn: string,
|
||||
options: SearchOptions,
|
||||
f: SearchCallback,
|
||||
f: (entry: SearchEntry) => void,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LdapOrgEntityProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export class LdapOrgEntityProvider implements EntityProvider {
|
||||
constructor(options: {
|
||||
@@ -140,12 +116,9 @@ export class LdapOrgEntityProvider implements EntityProvider {
|
||||
): LdapOrgEntityProvider;
|
||||
// (undocumented)
|
||||
getProviderName(): string;
|
||||
// (undocumented)
|
||||
read(): Promise<void>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LdapOrgReaderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export class LdapOrgReaderProcessor implements CatalogProcessor {
|
||||
constructor(options: {
|
||||
@@ -171,8 +144,6 @@ export class LdapOrgReaderProcessor implements CatalogProcessor {
|
||||
): Promise<boolean>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LdapProviderConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type LdapProviderConfig = {
|
||||
target: string;
|
||||
@@ -181,8 +152,6 @@ export type LdapProviderConfig = {
|
||||
groups: GroupConfig;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LdapVendor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type LdapVendor = {
|
||||
dnAttributeName: string;
|
||||
@@ -190,12 +159,6 @@ export type LdapVendor = {
|
||||
decodeStringAttribute: (entry: SearchEntry, name: string) => string[];
|
||||
};
|
||||
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (ae-missing-release-tag) "mapStringAttr" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export function mapStringAttr(
|
||||
entry: SearchEntry,
|
||||
@@ -204,18 +167,9 @@ export function mapStringAttr(
|
||||
setter: (value: string) => void,
|
||||
): void;
|
||||
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (ae-missing-release-tag) "readLdapConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export function readLdapConfig(config: Config): LdapProviderConfig[];
|
||||
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (ae-missing-release-tag) "readLdapOrg" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export function readLdapOrg(
|
||||
client: LdapClient,
|
||||
@@ -231,8 +185,6 @@ export function readLdapOrg(
|
||||
groups: GroupEntity[];
|
||||
}>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "UserConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type UserConfig = {
|
||||
dn: string;
|
||||
@@ -251,21 +203,10 @@ export type UserConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (tsdoc-undefined-tag) The TSDoc tag "@return" is not defined in this configuration
|
||||
// Warning: (ae-missing-release-tag) "UserTransformer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type UserTransformer = (
|
||||
vendor: LdapVendor,
|
||||
config: UserConfig,
|
||||
user: SearchEntry,
|
||||
) => Promise<UserEntity | undefined>;
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/ldap/vendors.d.ts:17:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/ldap/vendors.d.ts:18:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
```
|
||||
|
||||
@@ -25,14 +25,12 @@ import {
|
||||
LdapVendor,
|
||||
} from './vendors';
|
||||
|
||||
export interface SearchCallback {
|
||||
(entry: SearchEntry): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic wrapper for the ldapjs library.
|
||||
* Basic wrapper for the `ldapjs` library.
|
||||
*
|
||||
* Helps out with promisifying calls, paging, binding etc.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class LdapClient {
|
||||
private vendor: Promise<LdapVendor> | undefined;
|
||||
@@ -75,8 +73,8 @@ export class LdapClient {
|
||||
/**
|
||||
* Performs an LDAP search operation.
|
||||
*
|
||||
* @param dn The fully qualified base DN to search within
|
||||
* @param options The search options
|
||||
* @param dn - The fully qualified base DN to search within
|
||||
* @param options - The search options
|
||||
*/
|
||||
async search(dn: string, options: SearchOptions): Promise<SearchEntry[]> {
|
||||
try {
|
||||
@@ -128,14 +126,14 @@ export class LdapClient {
|
||||
/**
|
||||
* Performs an LDAP search operation, calls a function on each entry to limit memory usage
|
||||
*
|
||||
* @param dn The fully qualified base DN to search within
|
||||
* @param options The search options
|
||||
* @param f The callback to call on each search entry
|
||||
* @param dn - The fully qualified base DN to search within
|
||||
* @param options - The search options
|
||||
* @param f - The callback to call on each search entry
|
||||
*/
|
||||
async searchStreaming(
|
||||
dn: string,
|
||||
options: SearchOptions,
|
||||
f: SearchCallback,
|
||||
f: (entry: SearchEntry) => void,
|
||||
): Promise<void> {
|
||||
try {
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
|
||||
@@ -209,4 +209,88 @@ describe('readLdapConfig', () => {
|
||||
const expected = '(|(cn=foo bar)(cn=bar))';
|
||||
expect(actual[0].users.options.filter).toEqual(expected);
|
||||
});
|
||||
|
||||
it('supports a dot nested set structure', () => {
|
||||
const config = {
|
||||
providers: [
|
||||
{
|
||||
target: 'target',
|
||||
users: {
|
||||
dn: 'udn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
'metadata.annotations': {
|
||||
a: 'b',
|
||||
},
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
x: { a: 'b' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const actual = readLdapConfig(new ConfigReader(config));
|
||||
|
||||
expect(actual[0].users.set).toEqual({ 'metadata.annotations': { a: 'b' } });
|
||||
});
|
||||
|
||||
it('throws on attempts to modify the set structure', () => {
|
||||
const config = {
|
||||
providers: [
|
||||
{
|
||||
target: 'target',
|
||||
users: {
|
||||
dn: 'udn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
x: { a: 'b' },
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
x: { a: 'b' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const actual = readLdapConfig(new ConfigReader(config));
|
||||
|
||||
expect(() => {
|
||||
(actual[0].users.set as any).y = 2;
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot add property y, object is not extensible"`,
|
||||
);
|
||||
expect(() => {
|
||||
(actual[0].users.set as any).x.b = 2;
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot add property b, object is not extensible"`,
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
(actual[0].groups.set as any).y = 2;
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot add property y, object is not extensible"`,
|
||||
);
|
||||
expect(() => {
|
||||
(actual[0].groups.set as any).x.b = 2;
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot add property b, object is not extensible"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,8 @@ import { trimEnd } from 'lodash';
|
||||
|
||||
/**
|
||||
* The configuration parameters for a single LDAP provider.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type LdapProviderConfig = {
|
||||
// The prefix of the target that this matches on, e.g.
|
||||
@@ -39,6 +41,8 @@ export type LdapProviderConfig = {
|
||||
|
||||
/**
|
||||
* The settings to use for the a command.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BindConfig = {
|
||||
// The DN of the user to auth as, e.g.
|
||||
@@ -50,6 +54,8 @@ export type BindConfig = {
|
||||
|
||||
/**
|
||||
* The settings that govern the reading and interpretation of users.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type UserConfig = {
|
||||
// The DN under which users are stored.
|
||||
@@ -88,6 +94,8 @@ export type UserConfig = {
|
||||
|
||||
/**
|
||||
* The settings that govern the reading and interpretation of groups.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type GroupConfig = {
|
||||
// The DN under which groups are stored.
|
||||
@@ -163,9 +171,20 @@ const defaultConfig = {
|
||||
/**
|
||||
* Parses configuration.
|
||||
*
|
||||
* @param config The root of the LDAP config hierarchy
|
||||
* @param config - The root of the LDAP config hierarchy
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function readLdapConfig(config: Config): LdapProviderConfig[] {
|
||||
function freeze<T>(data: T): T {
|
||||
return JSON.parse(JSON.stringify(data), (_key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
Object.freeze(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
function readBindConfig(
|
||||
c: Config | undefined,
|
||||
): LdapProviderConfig['bind'] | undefined {
|
||||
@@ -217,7 +236,7 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] {
|
||||
if (!c) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.fromEntries(c.keys().map(path => [path, c.get(path)]));
|
||||
return c.get();
|
||||
}
|
||||
|
||||
function readUserMapConfig(
|
||||
@@ -297,6 +316,6 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] {
|
||||
// Replace arrays instead of merging, otherwise default behavior
|
||||
return Array.isArray(from) ? from : undefined;
|
||||
});
|
||||
return merged as LdapProviderConfig;
|
||||
return freeze(merged) as LdapProviderConfig;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
* example, for an item with the fully qualified DN
|
||||
* uid=john,ou=people,ou=spotify,dc=spotify,dc=net the generated entity would
|
||||
* have this annotation, with the value "john".
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const LDAP_RDN_ANNOTATION = 'backstage.io/ldap-rdn';
|
||||
|
||||
@@ -33,6 +35,8 @@ export const LDAP_RDN_ANNOTATION = 'backstage.io/ldap-rdn';
|
||||
* for an item with the DN uid=john,ou=people,ou=spotify,dc=spotify,dc=net the
|
||||
* generated entity would have this annotation, with that full string as its
|
||||
* value.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const LDAP_DN_ANNOTATION = 'backstage.io/ldap-dn';
|
||||
|
||||
@@ -44,5 +48,7 @@ export const LDAP_DN_ANNOTATION = 'backstage.io/ldap-dn';
|
||||
* for an item with the UUID 76ef928a-b251-1037-9840-d78227f36a7e, the
|
||||
* generated entity would have this annotation, with that full string as its
|
||||
* value.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const LDAP_UUID_ANNOTATION = 'backstage.io/ldap-uuid';
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
export { LdapClient } from './client';
|
||||
export { mapStringAttr } from './util';
|
||||
export { readLdapConfig } from './config';
|
||||
export type { LdapProviderConfig, GroupConfig, UserConfig } from './config';
|
||||
export type {
|
||||
LdapProviderConfig,
|
||||
GroupConfig,
|
||||
UserConfig,
|
||||
BindConfig,
|
||||
} from './config';
|
||||
export type { LdapVendor } from './vendors';
|
||||
export {
|
||||
LDAP_DN_ANNOTATION,
|
||||
|
||||
@@ -25,7 +25,13 @@ import {
|
||||
LDAP_RDN_ANNOTATION,
|
||||
LDAP_UUID_ANNOTATION,
|
||||
} from './constants';
|
||||
import { readLdapGroups, readLdapUsers, resolveRelations } from './read';
|
||||
import {
|
||||
defaultGroupTransformer,
|
||||
defaultUserTransformer,
|
||||
readLdapGroups,
|
||||
readLdapUsers,
|
||||
resolveRelations,
|
||||
} from './read';
|
||||
import { ActiveDirectoryVendor, DefaultLdapVendor } from './vendors';
|
||||
|
||||
function user(data: RecursivePartial<UserEntity>): UserEntity {
|
||||
@@ -264,6 +270,7 @@ describe('readLdapGroups', () => {
|
||||
new Map([['dn-value', new Set(['x', 'y', 'z'])]]),
|
||||
);
|
||||
});
|
||||
|
||||
it('transfers all attributes from Microsoft Active Directory', async () => {
|
||||
client.getVendor.mockResolvedValue(ActiveDirectoryVendor);
|
||||
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
|
||||
@@ -358,6 +365,7 @@ describe('resolveRelations', () => {
|
||||
expect(parent.spec.children).toEqual(['child']);
|
||||
expect(child.spec.parent).toEqual('parent');
|
||||
});
|
||||
|
||||
it('matches by UUID', () => {
|
||||
const parent = group({
|
||||
metadata: {
|
||||
@@ -539,3 +547,162 @@ describe('resolveRelations', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultUserTransformer', () => {
|
||||
it('can set things safely', async () => {
|
||||
const config: UserConfig = {
|
||||
dn: 'ddd',
|
||||
options: {},
|
||||
map: {
|
||||
rdn: 'uid',
|
||||
name: 'uid',
|
||||
displayName: 'cn',
|
||||
email: 'mail',
|
||||
memberOf: 'memberOf',
|
||||
},
|
||||
set: {
|
||||
'metadata.annotations.a': 1,
|
||||
'metadata.annotations': { a: 2, b: 3 },
|
||||
},
|
||||
};
|
||||
|
||||
const entry = searchEntry({
|
||||
uid: ['uid-value'],
|
||||
description: ['description-value'],
|
||||
cn: ['cn-value'],
|
||||
mail: ['mail-value'],
|
||||
avatarUrl: ['avatarUrl-value'],
|
||||
memberOf: ['x', 'y', 'z'],
|
||||
entryDN: ['dn-value'],
|
||||
entryUUID: ['uuid-value'],
|
||||
});
|
||||
|
||||
let output = await defaultUserTransformer(DefaultLdapVendor, config, entry);
|
||||
expect(output).toEqual({
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/ldap-dn': 'dn-value',
|
||||
'backstage.io/ldap-rdn': 'uid-value',
|
||||
'backstage.io/ldap-uuid': 'uuid-value',
|
||||
a: 2,
|
||||
b: 3,
|
||||
},
|
||||
name: 'uid-value',
|
||||
},
|
||||
spec: {
|
||||
memberOf: [],
|
||||
profile: { displayName: 'cn-value', email: 'mail-value' },
|
||||
},
|
||||
});
|
||||
|
||||
(output!.metadata.annotations as any).c = 7;
|
||||
|
||||
// exact same inputs again
|
||||
output = await defaultUserTransformer(DefaultLdapVendor, config, entry);
|
||||
expect(output).toEqual({
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/ldap-dn': 'dn-value',
|
||||
'backstage.io/ldap-rdn': 'uid-value',
|
||||
'backstage.io/ldap-uuid': 'uuid-value',
|
||||
a: 2,
|
||||
b: 3,
|
||||
},
|
||||
name: 'uid-value',
|
||||
},
|
||||
spec: {
|
||||
memberOf: [],
|
||||
profile: { displayName: 'cn-value', email: 'mail-value' },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultGroupTransformer', () => {
|
||||
it('can set things safely', async () => {
|
||||
const config: GroupConfig = {
|
||||
dn: 'ddd',
|
||||
options: {},
|
||||
map: {
|
||||
rdn: 'uid',
|
||||
name: 'uid',
|
||||
displayName: 'cn',
|
||||
email: 'mail',
|
||||
description: 'description',
|
||||
type: 'type',
|
||||
members: 'members',
|
||||
memberOf: 'memberOf',
|
||||
},
|
||||
set: {
|
||||
'metadata.annotations.a': 1,
|
||||
'metadata.annotations': { a: 2, b: 3 },
|
||||
},
|
||||
};
|
||||
|
||||
const entry = searchEntry({
|
||||
uid: ['uid-value'],
|
||||
description: ['description-value'],
|
||||
cn: ['cn-value'],
|
||||
mail: ['mail-value'],
|
||||
avatarUrl: ['avatarUrl-value'],
|
||||
memberOf: ['x', 'y', 'z'],
|
||||
entryDN: ['dn-value'],
|
||||
entryUUID: ['uuid-value'],
|
||||
});
|
||||
|
||||
let output = await defaultGroupTransformer(
|
||||
DefaultLdapVendor,
|
||||
config,
|
||||
entry,
|
||||
);
|
||||
expect(output).toEqual({
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/ldap-dn': 'dn-value',
|
||||
'backstage.io/ldap-rdn': 'uid-value',
|
||||
'backstage.io/ldap-uuid': 'uuid-value',
|
||||
a: 2,
|
||||
b: 3,
|
||||
},
|
||||
description: 'description-value',
|
||||
name: 'uid-value',
|
||||
},
|
||||
spec: {
|
||||
type: 'unknown',
|
||||
children: [],
|
||||
profile: { displayName: 'cn-value', email: 'mail-value' },
|
||||
},
|
||||
});
|
||||
|
||||
(output!.metadata.annotations as any).c = 7;
|
||||
|
||||
// exact same inputs again
|
||||
output = await defaultGroupTransformer(DefaultLdapVendor, config, entry);
|
||||
expect(output).toEqual({
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/ldap-dn': 'dn-value',
|
||||
'backstage.io/ldap-rdn': 'uid-value',
|
||||
'backstage.io/ldap-uuid': 'uuid-value',
|
||||
a: 2,
|
||||
b: 3,
|
||||
},
|
||||
description: 'description-value',
|
||||
name: 'uid-value',
|
||||
},
|
||||
spec: {
|
||||
type: 'unknown',
|
||||
children: [],
|
||||
profile: { displayName: 'cn-value', email: 'mail-value' },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
|
||||
import { SearchEntry } from 'ldapjs';
|
||||
import lodashSet from 'lodash/set';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { buildOrgHierarchy } from './org';
|
||||
import { LdapClient } from './client';
|
||||
import { GroupConfig, UserConfig } from './config';
|
||||
@@ -30,6 +31,12 @@ import { Logger } from 'winston';
|
||||
import { GroupTransformer, UserTransformer } from './types';
|
||||
import { mapStringAttr } from './util';
|
||||
|
||||
/**
|
||||
* The default implementation of the transformation from an LDAP entry to a
|
||||
* User entity.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export async function defaultUserTransformer(
|
||||
vendor: LdapVendor,
|
||||
config: UserConfig,
|
||||
@@ -52,7 +59,7 @@ export async function defaultUserTransformer(
|
||||
|
||||
if (set) {
|
||||
for (const [path, value] of Object.entries(set)) {
|
||||
lodashSet(entity, path, value);
|
||||
lodashSet(entity, path, cloneDeep(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,9 +94,9 @@ export async function defaultUserTransformer(
|
||||
/**
|
||||
* Reads users out of an LDAP provider.
|
||||
*
|
||||
* @param client The LDAP client
|
||||
* @param config The user data configuration
|
||||
* @param opts
|
||||
* @param client - The LDAP client
|
||||
* @param config - The user data configuration
|
||||
* @param opts - Additional options
|
||||
*/
|
||||
export async function readLdapUsers(
|
||||
client: LdapClient,
|
||||
@@ -124,6 +131,12 @@ export async function readLdapUsers(
|
||||
return { users: entities, userMemberOf };
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of the transformation from an LDAP entry to a
|
||||
* Group entity.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export async function defaultGroupTransformer(
|
||||
vendor: LdapVendor,
|
||||
config: GroupConfig,
|
||||
@@ -146,7 +159,7 @@ export async function defaultGroupTransformer(
|
||||
|
||||
if (set) {
|
||||
for (const [path, value] of Object.entries(set)) {
|
||||
lodashSet(entity, path, value);
|
||||
lodashSet(entity, path, cloneDeep(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,9 +197,9 @@ export async function defaultGroupTransformer(
|
||||
/**
|
||||
* Reads groups out of an LDAP provider.
|
||||
*
|
||||
* @param client The LDAP client
|
||||
* @param config The group data configuration
|
||||
* @param opts
|
||||
* @param client - The LDAP client
|
||||
* @param config - The group data configuration
|
||||
* @param opts - Additional options
|
||||
*/
|
||||
export async function readLdapGroups(
|
||||
client: LdapClient,
|
||||
@@ -239,13 +252,12 @@ export async function readLdapGroups(
|
||||
/**
|
||||
* Reads users and groups out of an LDAP provider.
|
||||
*
|
||||
* Invokes the above "raw" read functions and stitches together the results
|
||||
* with all relations etc filled in.
|
||||
* @param client - The LDAP client
|
||||
* @param userConfig - The user data configuration
|
||||
* @param groupConfig - The group data configuration
|
||||
* @param options - Additional options
|
||||
*
|
||||
* @param client The LDAP client
|
||||
* @param userConfig The user data configuration
|
||||
* @param groupConfig The group data configuration
|
||||
* @param options
|
||||
* @public
|
||||
*/
|
||||
export async function readLdapOrg(
|
||||
client: LdapClient,
|
||||
@@ -260,6 +272,9 @@ export async function readLdapOrg(
|
||||
users: UserEntity[];
|
||||
groups: GroupEntity[];
|
||||
}> {
|
||||
// Invokes the above "raw" read functions and stitches together the results
|
||||
// with all relations etc filled in.
|
||||
|
||||
const { users, userMemberOf } = await readLdapUsers(client, userConfig, {
|
||||
transformer: options?.userTransformer,
|
||||
});
|
||||
@@ -320,14 +335,14 @@ function ensureItems(
|
||||
* Takes groups and entities with empty relations, and fills in the various
|
||||
* relations that were returned by the readers, and forms the org hierarchy.
|
||||
*
|
||||
* @param groups Group entities with empty relations; modified in place
|
||||
* @param users User entities with empty relations; modified in place
|
||||
* @param userMemberOf For a user DN, the set of group DNs or UUIDs that the
|
||||
* user is a member of
|
||||
* @param groupMemberOf For a group DN, the set of group DNs or UUIDs that the
|
||||
* group is a member of (parents in the hierarchy)
|
||||
* @param groupMember For a group DN, the set of group DNs or UUIDs that are
|
||||
* members of the group (children in the hierarchy)
|
||||
* @param groups - Group entities with empty relations; modified in place
|
||||
* @param users - User entities with empty relations; modified in place
|
||||
* @param userMemberOf - For a user DN, the set of group DNs or UUIDs that the
|
||||
* user is a member of
|
||||
* @param groupMemberOf - For a group DN, the set of group DNs or UUIDs that
|
||||
* the group is a member of (parents in the hierarchy)
|
||||
* @param groupMember - For a group DN, the set of group DNs or UUIDs that are
|
||||
* members of the group (children in the hierarchy)
|
||||
*/
|
||||
export function resolveRelations(
|
||||
groups: GroupEntity[],
|
||||
|
||||
@@ -21,10 +21,15 @@ import { GroupConfig, UserConfig } from './config';
|
||||
/**
|
||||
* Customize the ingested User entity
|
||||
*
|
||||
* @param vendor The LDAP vendor that can be used to find and decode vendor specific attributes
|
||||
* @param config The User specific config used by the default transformer.
|
||||
* @param user The found LDAP entry in its source format. This is the entry that you want to transform
|
||||
* @return A `UserEntity` or `undefined` if you want to ignore the found user for being ingested by the catalog
|
||||
* @param vendor - The LDAP vendor that can be used to find and decode vendor
|
||||
* specific attributes
|
||||
* @param config - The User specific config used by the default transformer.
|
||||
* @param user - The found LDAP entry in its source format. This is the entry
|
||||
* that you want to transform
|
||||
* @returns A `UserEntity` or `undefined` if you want to ignore the found user
|
||||
* for being ingested by the catalog
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type UserTransformer = (
|
||||
vendor: LdapVendor,
|
||||
@@ -35,10 +40,15 @@ export type UserTransformer = (
|
||||
/**
|
||||
* Customize the ingested Group entity
|
||||
*
|
||||
* @param vendor The LDAP vendor that can be used to find and decode vendor specific attributes
|
||||
* @param config The Group specific config used by the default transformer.
|
||||
* @param group The found LDAP entry in its source format. This is the entry that you want to transform
|
||||
* @return A `GroupEntity` or `undefined` if you want to ignore the found group for being ingested by the catalog
|
||||
* @param vendor - The LDAP vendor that can be used to find and decode vendor
|
||||
* specific attributes
|
||||
* @param config - The Group specific config used by the default transformer.
|
||||
* @param group - The found LDAP entry in its source format. This is the entry
|
||||
* that you want to transform
|
||||
* @returns A `GroupEntity` or `undefined` if you want to ignore the found group
|
||||
* for being ingested by the catalog
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type GroupTransformer = (
|
||||
vendor: LdapVendor,
|
||||
|
||||
@@ -20,19 +20,25 @@ import { LdapVendor } from './vendors';
|
||||
/**
|
||||
* Builds a string form of an LDAP Error structure.
|
||||
*
|
||||
* @param error The error
|
||||
* @param error - The error
|
||||
*/
|
||||
export function errorString(error: LDAPError) {
|
||||
return `${error.code} ${error.name}: ${error.message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a single-valued attribute to a consumer
|
||||
* Maps a single-valued attribute to a consumer.
|
||||
*
|
||||
* @param entry The LDAP source entry
|
||||
* @param vendor The LDAP vendor
|
||||
* @param attributeName The source attribute to map. If the attribute is undefined the mapping will be silently ignored.
|
||||
* @param setter The function to be called with the decoded attribute from the source entry
|
||||
* This helper can be useful when implementing a user or group transformer.
|
||||
*
|
||||
* @param entry - The LDAP source entry
|
||||
* @param vendor - The LDAP vendor
|
||||
* @param attributeName - The source attribute to map. If the attribute is
|
||||
* undefined the mapping will be silently ignored.
|
||||
* @param setter - The function to be called with the decoded attribute from the
|
||||
* source entry
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function mapStringAttr(
|
||||
entry: SearchEntry,
|
||||
|
||||
@@ -18,6 +18,8 @@ import { SearchEntry } from 'ldapjs';
|
||||
|
||||
/**
|
||||
* An LDAP Vendor handles unique nuances between different vendors.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type LdapVendor = {
|
||||
/**
|
||||
@@ -31,8 +33,8 @@ export type LdapVendor = {
|
||||
/**
|
||||
* Decode ldap entry values for a given attribute name to their string representation.
|
||||
*
|
||||
* @param entry The ldap entry
|
||||
* @param name The attribute to decode
|
||||
* @param entry - The ldap entry
|
||||
* @param name - The attribute to decode
|
||||
*/
|
||||
decodeStringAttribute: (entry: SearchEntry, name: string) => string[];
|
||||
};
|
||||
|
||||
@@ -39,6 +39,13 @@ import {
|
||||
/**
|
||||
* Reads user and group entries out of an LDAP service, and provides them as
|
||||
* User and Group entities for the catalog.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Add an instance of this class to your catalog builder, and then periodically
|
||||
* call the {@link LdapOrgEntityProvider.read} method.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class LdapOrgEntityProvider implements EntityProvider {
|
||||
private connection?: EntityProviderConnection;
|
||||
@@ -113,14 +120,20 @@ export class LdapOrgEntityProvider implements EntityProvider {
|
||||
},
|
||||
) {}
|
||||
|
||||
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */
|
||||
getProviderName() {
|
||||
return `LdapOrgEntityProvider:${this.options.id}`;
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
|
||||
async connect(connection: EntityProviderConnection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one complete ingestion loop. Call this method regularly at some
|
||||
* appropriate cadence.
|
||||
*/
|
||||
async read() {
|
||||
if (!this.connection) {
|
||||
throw new Error('Not initialized');
|
||||
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
|
||||
/**
|
||||
* Extracts teams and users out of an LDAP server.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class LdapOrgReaderProcessor implements CatalogProcessor {
|
||||
private readonly providers: LdapProviderConfig[];
|
||||
|
||||
@@ -16,51 +16,37 @@ import * as msal from '@azure/msal-node';
|
||||
import { Response as Response_2 } from 'node-fetch';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "defaultGroupTransformer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function defaultGroupTransformer(
|
||||
group: MicrosoftGraph.Group,
|
||||
groupPhoto?: string,
|
||||
): Promise<GroupEntity | undefined>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "defaultOrganizationTransformer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function defaultOrganizationTransformer(
|
||||
organization: MicrosoftGraph.Organization,
|
||||
): Promise<GroupEntity | undefined>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "defaultUserTransformer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function defaultUserTransformer(
|
||||
user: MicrosoftGraph.User,
|
||||
userPhoto?: string,
|
||||
): Promise<UserEntity | undefined>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "GroupTransformer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type GroupTransformer = (
|
||||
group: MicrosoftGraph.Group,
|
||||
groupPhoto?: string,
|
||||
) => Promise<GroupEntity | undefined>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "MICROSOFT_GRAPH_GROUP_ID_ANNOTATION" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const MICROSOFT_GRAPH_GROUP_ID_ANNOTATION =
|
||||
'graph.microsoft.com/group-id';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "MICROSOFT_GRAPH_TENANT_ID_ANNOTATION" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const MICROSOFT_GRAPH_TENANT_ID_ANNOTATION =
|
||||
'graph.microsoft.com/tenant-id';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "MICROSOFT_GRAPH_USER_ID_ANNOTATION" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = 'graph.microsoft.com/user-id';
|
||||
|
||||
@@ -76,7 +62,6 @@ export class MicrosoftGraphClient {
|
||||
groupId: string,
|
||||
maxSize: number,
|
||||
): Promise<string | undefined>;
|
||||
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-catalog-backend-module-msgraph" does not have an export "ODataQuery"
|
||||
getGroups(query?: ODataQuery): AsyncIterable<MicrosoftGraph.Group>;
|
||||
getOrganization(tenantId: string): Promise<MicrosoftGraph.Organization>;
|
||||
// (undocumented)
|
||||
@@ -86,18 +71,12 @@ export class MicrosoftGraphClient {
|
||||
maxSize: number,
|
||||
): Promise<string | undefined>;
|
||||
getUserProfile(userId: string): Promise<MicrosoftGraph.User>;
|
||||
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-catalog-backend-module-msgraph" does not have an export "ODataQuery"
|
||||
getUsers(query?: ODataQuery): AsyncIterable<MicrosoftGraph.User>;
|
||||
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-catalog-backend-module-msgraph" does not have an export "ODataQuery"
|
||||
requestApi(path: string, query?: ODataQuery): Promise<Response_2>;
|
||||
// Warning: (ae-forgotten-export) The symbol "ODataQuery" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-catalog-backend-module-msgraph" does not have an export "ODataQuery"
|
||||
requestCollection<T>(path: string, query?: ODataQuery): AsyncIterable<T>;
|
||||
requestRaw(url: string): Promise<Response_2>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "MicrosoftGraphOrgEntityProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
constructor(options: {
|
||||
@@ -124,12 +103,9 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
): MicrosoftGraphOrgEntityProvider;
|
||||
// (undocumented)
|
||||
getProviderName(): string;
|
||||
// (undocumented)
|
||||
read(): Promise<void>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "MicrosoftGraphOrgReaderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {
|
||||
constructor(options: {
|
||||
@@ -157,8 +133,6 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {
|
||||
): Promise<boolean>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "MicrosoftGraphProviderConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type MicrosoftGraphProviderConfig = {
|
||||
target: string;
|
||||
@@ -171,28 +145,27 @@ export type MicrosoftGraphProviderConfig = {
|
||||
groupFilter?: string;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "normalizeEntityName" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function normalizeEntityName(name: string): string;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "OrganizationTransformer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type ODataQuery = {
|
||||
filter?: string;
|
||||
expand?: string[];
|
||||
select?: string[];
|
||||
};
|
||||
|
||||
// @public
|
||||
export type OrganizationTransformer = (
|
||||
organization: MicrosoftGraph.Organization,
|
||||
) => Promise<GroupEntity | undefined>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "readMicrosoftGraphConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function readMicrosoftGraphConfig(
|
||||
config: Config,
|
||||
): MicrosoftGraphProviderConfig[];
|
||||
|
||||
// Warning: (ae-missing-release-tag) "readMicrosoftGraphOrg" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function readMicrosoftGraphOrg(
|
||||
client: MicrosoftGraphClient,
|
||||
tenantId: string,
|
||||
@@ -210,15 +183,9 @@ export function readMicrosoftGraphOrg(
|
||||
groups: GroupEntity[];
|
||||
}>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "UserTransformer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type UserTransformer = (
|
||||
user: MicrosoftGraph.User,
|
||||
userPhoto?: string,
|
||||
) => Promise<UserEntity | undefined>;
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/microsoftGraph/config.d.ts:28:8 - (tsdoc-undefined-tag) The TSDoc tag "@visibility" is not defined in this configuration
|
||||
```
|
||||
|
||||
@@ -41,6 +41,11 @@ export type ODataQuery = {
|
||||
select?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Extends the base msgraph types to include the odata type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type GroupMember =
|
||||
| (MicrosoftGraph.Group & { '@odata.type': '#microsoft.graph.user' })
|
||||
| (MicrosoftGraph.User & { '@odata.type': '#microsoft.graph.group' });
|
||||
|
||||
@@ -19,6 +19,8 @@ import { trimEnd } from 'lodash';
|
||||
|
||||
/**
|
||||
* The configuration parameters for a single Microsoft Graph provider.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type MicrosoftGraphProviderConfig = {
|
||||
/**
|
||||
@@ -42,8 +44,6 @@ export type MicrosoftGraphProviderConfig = {
|
||||
clientId: string;
|
||||
/**
|
||||
* The OAuth client secret to use for authenticating requests.
|
||||
*
|
||||
* @visibility secret
|
||||
*/
|
||||
clientSecret: string;
|
||||
/**
|
||||
@@ -66,6 +66,13 @@ export type MicrosoftGraphProviderConfig = {
|
||||
groupFilter?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses configuration.
|
||||
*
|
||||
* @param config - The root of the msgraph config hierarchy
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function readMicrosoftGraphConfig(
|
||||
config: Config,
|
||||
): MicrosoftGraphProviderConfig[] {
|
||||
|
||||
@@ -16,17 +16,23 @@
|
||||
|
||||
/**
|
||||
* The tenant id used by the Microsoft Graph API
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const MICROSOFT_GRAPH_TENANT_ID_ANNOTATION =
|
||||
'graph.microsoft.com/tenant-id';
|
||||
|
||||
/**
|
||||
* The group id used by the Microsoft Graph API
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const MICROSOFT_GRAPH_GROUP_ID_ANNOTATION =
|
||||
'graph.microsoft.com/group-id';
|
||||
|
||||
/**
|
||||
* The user id used by the Microsoft Graph API
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = 'graph.microsoft.com/user-id';
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Takes an input string and cleans it up to become suitable as an entity name.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function normalizeEntityName(name: string): string {
|
||||
let cleaned = name
|
||||
.trim()
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { MicrosoftGraphClient } from './client';
|
||||
export type { ODataQuery } from './client';
|
||||
export { readMicrosoftGraphConfig } from './config';
|
||||
export type { MicrosoftGraphProviderConfig } from './config';
|
||||
export {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
GroupEntity,
|
||||
stringifyEntityRef,
|
||||
@@ -35,6 +36,12 @@ import {
|
||||
UserTransformer,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* The default implementation of the transformation from a graph user entry to
|
||||
* a User entity.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export async function defaultUserTransformer(
|
||||
user: MicrosoftGraph.User,
|
||||
userPhoto?: string,
|
||||
@@ -208,6 +215,12 @@ export async function readMicrosoftGraphUsersInGroups(
|
||||
return { users };
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of the transformation from a graph organization
|
||||
* entry to a Group entity.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export async function defaultOrganizationTransformer(
|
||||
organization: MicrosoftGraph.Organization,
|
||||
): Promise<GroupEntity | undefined> {
|
||||
@@ -258,6 +271,12 @@ function extractGroupName(group: MicrosoftGraph.Group): string {
|
||||
return (group.mailNickname || group.displayName) as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of the transformation from a graph group entry to
|
||||
* a Group entity.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export async function defaultGroupTransformer(
|
||||
group: MicrosoftGraph.Group,
|
||||
groupPhoto?: string,
|
||||
@@ -472,6 +491,11 @@ export function resolveRelations(
|
||||
buildMemberOf(groups, users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an entire org as Group and User entities.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export async function readMicrosoftGraphOrg(
|
||||
client: MicrosoftGraphClient,
|
||||
tenantId: string,
|
||||
|
||||
@@ -17,15 +17,30 @@
|
||||
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
|
||||
/**
|
||||
* Customize the ingested User entity
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type UserTransformer = (
|
||||
user: MicrosoftGraph.User,
|
||||
userPhoto?: string,
|
||||
) => Promise<UserEntity | undefined>;
|
||||
|
||||
/**
|
||||
* Customize the ingested organization Group entity
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type OrganizationTransformer = (
|
||||
organization: MicrosoftGraph.Organization,
|
||||
) => Promise<GroupEntity | undefined>;
|
||||
|
||||
/**
|
||||
* Customize the ingested Group entity
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type GroupTransformer = (
|
||||
group: MicrosoftGraph.Group,
|
||||
groupPhoto?: string,
|
||||
|
||||
+9
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
LOCATION_ANNOTATION,
|
||||
@@ -41,6 +42,8 @@ import {
|
||||
/**
|
||||
* Reads user and group entries out of Microsoft Graph, and provides them as
|
||||
* User and Group entities for the catalog.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
private connection?: EntityProviderConnection;
|
||||
@@ -91,14 +94,20 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
},
|
||||
) {}
|
||||
|
||||
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */
|
||||
getProviderName() {
|
||||
return `MicrosoftGraphOrgEntityProvider:${this.options.id}`;
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
|
||||
async connect(connection: EntityProviderConnection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one complete ingestion loop. Call this method regularly at some
|
||||
* appropriate cadence.
|
||||
*/
|
||||
async read() {
|
||||
if (!this.connection) {
|
||||
throw new Error('Not initialized');
|
||||
|
||||
+2
@@ -34,6 +34,8 @@ import {
|
||||
|
||||
/**
|
||||
* Extracts teams and users out of a the Microsoft Graph API.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {
|
||||
private readonly providers: MicrosoftGraphProviderConfig[];
|
||||
|
||||
@@ -25,6 +25,7 @@ import { Location as Location_2 } from '@backstage/catalog-model';
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import { Organizations } from 'aws-sdk';
|
||||
import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
|
||||
import { PermissionRule } from '@backstage/plugin-permission-node';
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
@@ -302,14 +303,9 @@ export type CatalogEnvironment = {
|
||||
database: PluginDatabaseManager;
|
||||
config: Config;
|
||||
reader: UrlReader;
|
||||
permissions: PermissionAuthorizer;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type CatalogPermissionRule = PermissionRule<
|
||||
Entity,
|
||||
EntitiesSearchFilter
|
||||
>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "CatalogProcessingEngine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
@@ -526,6 +522,11 @@ export class CommonDatabase implements Database {
|
||||
): Promise<DbEntityResponse>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const createCatalogPermissionRule: <TParams extends unknown[]>(
|
||||
rule: PermissionRule<Entity, EntitiesSearchFilter, TParams>,
|
||||
) => PermissionRule<Entity, EntitiesSearchFilter, TParams>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "CreateDatabaseOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public @deprecated (undocumented)
|
||||
@@ -1309,7 +1310,13 @@ export class NextCatalogBuilder {
|
||||
addEntityPolicy(...policies: EntityPolicy[]): NextCatalogBuilder;
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
addEntityProvider(...providers: EntityProvider[]): NextCatalogBuilder;
|
||||
addPermissionRules(...permissionRules: CatalogPermissionRule[]): void;
|
||||
addPermissionRules(
|
||||
...permissionRules: PermissionRule<
|
||||
Entity,
|
||||
EntitiesSearchFilter,
|
||||
unknown[]
|
||||
>[]
|
||||
): void;
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
addProcessor(...processors: CatalogProcessor[]): NextCatalogBuilder;
|
||||
build(): Promise<{
|
||||
@@ -1357,7 +1364,7 @@ export interface NextRouterOptions {
|
||||
// (undocumented)
|
||||
logger: Logger_2;
|
||||
// (undocumented)
|
||||
permissionRules?: CatalogPermissionRule[];
|
||||
permissionRules?: PermissionRule<Entity, EntitiesSearchFilter, unknown[]>[];
|
||||
// (undocumented)
|
||||
refreshService?: RefreshService;
|
||||
}
|
||||
@@ -1392,12 +1399,28 @@ export function parseEntityYaml(
|
||||
|
||||
// @public
|
||||
export const permissionRules: {
|
||||
hasAnnotation: CatalogPermissionRule;
|
||||
hasLabel: CatalogPermissionRule;
|
||||
hasMetadata: CatalogPermissionRule;
|
||||
hasSpec: CatalogPermissionRule;
|
||||
isEntityKind: CatalogPermissionRule;
|
||||
isEntityOwner: CatalogPermissionRule;
|
||||
hasAnnotation: PermissionRule<
|
||||
Entity,
|
||||
EntitiesSearchFilter,
|
||||
[annotation: string]
|
||||
>;
|
||||
hasLabel: PermissionRule<Entity, EntitiesSearchFilter, [label: string]>;
|
||||
hasMetadata: PermissionRule<
|
||||
Entity,
|
||||
EntitiesSearchFilter,
|
||||
[key: string, value?: string | undefined]
|
||||
>;
|
||||
hasSpec: PermissionRule<
|
||||
Entity,
|
||||
EntitiesSearchFilter,
|
||||
[key: string, value?: string | undefined]
|
||||
>;
|
||||
isEntityKind: PermissionRule<Entity, EntitiesSearchFilter, [kinds: string[]]>;
|
||||
isEntityOwner: PermissionRule<
|
||||
Entity,
|
||||
EntitiesSearchFilter,
|
||||
[claims: string[]]
|
||||
>;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "PlaceholderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
@@ -1493,6 +1516,7 @@ export type RefreshIntervalFunction = () => number;
|
||||
// @public
|
||||
export type RefreshOptions = {
|
||||
entityRef: string;
|
||||
authorizationToken?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
@@ -32,11 +32,12 @@
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.10.1",
|
||||
"@backstage/catalog-client": "^0.5.3",
|
||||
"@backstage/plugin-catalog-common": "^0.1.0",
|
||||
"@backstage/catalog-model": "^0.9.8",
|
||||
"@backstage/config": "^0.1.11",
|
||||
"@backstage/errors": "^0.1.5",
|
||||
"@backstage/integration": "^0.7.0",
|
||||
"@backstage/plugin-catalog-common": "^0.1.0",
|
||||
"@backstage/plugin-permission-common": "^0.3.0",
|
||||
"@backstage/plugin-permission-node": "^0.2.3",
|
||||
"@backstage/search-common": "^0.2.1",
|
||||
"@backstage/types": "^0.1.1",
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger, UrlReader } from '@backstage/backend-common';
|
||||
import {
|
||||
getVoidLogger,
|
||||
PluginEndpointDiscovery,
|
||||
ServerTokenManager,
|
||||
UrlReader,
|
||||
} from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Knex } from 'knex';
|
||||
@@ -24,6 +29,7 @@ import { CatalogProcessorParser } from '../../ingestion';
|
||||
import * as result from '../../ingestion/processors/results';
|
||||
import { CatalogBuilder } from './CatalogBuilder';
|
||||
import { CatalogEnvironment } from '../../service';
|
||||
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
|
||||
const dummyEntity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
@@ -47,11 +53,25 @@ describe('CatalogBuilder', () => {
|
||||
readTree: jest.fn(),
|
||||
search: jest.fn(),
|
||||
};
|
||||
const config = new ConfigReader({});
|
||||
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
|
||||
const discovery: PluginEndpointDiscovery = {
|
||||
async getBaseUrl() {
|
||||
return mockBaseUrl;
|
||||
},
|
||||
async getExternalBaseUrl() {
|
||||
return mockBaseUrl;
|
||||
},
|
||||
};
|
||||
const env: CatalogEnvironment = {
|
||||
logger: getVoidLogger(),
|
||||
database: { getClient: async () => db },
|
||||
config: new ConfigReader({}),
|
||||
config,
|
||||
reader,
|
||||
permissions: ServerPermissionClient.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager: ServerTokenManager.noop(),
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -15,4 +15,3 @@
|
||||
*/
|
||||
|
||||
export * from './rules';
|
||||
export type { CatalogPermissionRule } from './types';
|
||||
|
||||
@@ -14,15 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntitiesSearchFilter } from '../../catalog/types';
|
||||
import { CatalogPermissionRule } from '../types';
|
||||
import { get } from 'lodash';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { createCatalogPermissionRule } from './util';
|
||||
|
||||
export function createPropertyRule(
|
||||
propertyType: 'metadata' | 'spec',
|
||||
): CatalogPermissionRule {
|
||||
return {
|
||||
export const createPropertyRule = (propertyType: 'metadata' | 'spec') =>
|
||||
createCatalogPermissionRule({
|
||||
name: `HAS_${propertyType.toUpperCase()}`,
|
||||
description: `Allow entities which have the specified ${propertyType} subfield.`,
|
||||
apply: (resource: Entity, key: string, value?: string) => {
|
||||
@@ -32,9 +29,8 @@ export function createPropertyRule(
|
||||
}
|
||||
return !!foundValue;
|
||||
},
|
||||
toQuery: (key: string, value?: string): EntitiesSearchFilter => ({
|
||||
toQuery: (key: string, value?: string) => ({
|
||||
key: `${propertyType}.${key}`,
|
||||
...(value !== undefined && { values: [value] }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,21 +15,21 @@
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntitiesSearchFilter } from '../../catalog/types';
|
||||
import { CatalogPermissionRule } from '../types';
|
||||
import { createCatalogPermissionRule } from './util';
|
||||
|
||||
/**
|
||||
* A {@link CatalogPermissionRule} which filters for the presence of an
|
||||
* annotation on a given entity.
|
||||
* A catalog {@link @backstage/plugin-permission-node#PermissionRule} which
|
||||
* filters for the presence of an annotation on a given entity.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const hasAnnotation: CatalogPermissionRule = {
|
||||
export const hasAnnotation = createCatalogPermissionRule({
|
||||
name: 'HAS_ANNOTATION',
|
||||
description:
|
||||
'Allow entities which are annotated with the specified annotation',
|
||||
apply: (resource: Entity, annotation: string) =>
|
||||
!!resource.metadata.annotations?.hasOwnProperty(annotation),
|
||||
toQuery: (annotation: string): EntitiesSearchFilter => ({
|
||||
toQuery: (annotation: string) => ({
|
||||
key: `metadata.annotations.${annotation}`,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -15,20 +15,19 @@
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntitiesSearchFilter } from '../../catalog/types';
|
||||
import { CatalogPermissionRule } from '../types';
|
||||
import { createCatalogPermissionRule } from './util';
|
||||
|
||||
/**
|
||||
* A {@link CatalogPermissionRule} which filters for entities with a specified
|
||||
* label in its metadata.
|
||||
* A catalog {@link @backstage/plugin-permission-node#PermissionRule} which
|
||||
* filters for entities with a specified label in its metadata.
|
||||
* @public
|
||||
*/
|
||||
export const hasLabel: CatalogPermissionRule = {
|
||||
export const hasLabel = createCatalogPermissionRule({
|
||||
name: 'HAS_LABEL',
|
||||
description: 'Allow entities which have the specified label metadata.',
|
||||
apply: (resource: Entity, label: string) =>
|
||||
!!resource.metadata.labels?.hasOwnProperty(label),
|
||||
toQuery: (label: string): EntitiesSearchFilter => ({
|
||||
toQuery: (label: string) => ({
|
||||
key: `metadata.labels.${label}`,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
import { createPropertyRule } from './createPropertyRule';
|
||||
|
||||
/**
|
||||
* A {@link CatalogPermissionRule} which filters for entities with the specified
|
||||
* metadata subfield. Also matches on values if value is provided.
|
||||
* A catalog {@link @backstage/plugin-permission-node#PermissionRule} which
|
||||
* filters for entities with the specified metadata subfield. Also matches on
|
||||
* values if value is provided.
|
||||
*
|
||||
* The key argument to the `apply` and `toQuery` methods can be nested, such as
|
||||
* 'field.nestedfield'.
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
import { createPropertyRule } from './createPropertyRule';
|
||||
|
||||
/**
|
||||
* A {@link CatalogPermissionRule} which filters for entities with the specified
|
||||
* spec subfield. Also matches on values if value is provided.
|
||||
* A catalog {@link @backstage/plugin-permission-node#PermissionRule} which
|
||||
* filters for entities with the specified spec subfield. Also matches on values
|
||||
* if value is provided.
|
||||
*
|
||||
* The key argument to the `apply` and `toQuery` methods can be nested, such as
|
||||
* 'field.nestedfield'.
|
||||
|
||||
@@ -34,3 +34,5 @@ export const permissionRules = {
|
||||
isEntityKind,
|
||||
isEntityOwner,
|
||||
};
|
||||
|
||||
export { createCatalogPermissionRule } from './util';
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
*/
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntitiesSearchFilter } from '../../catalog/types';
|
||||
import { CatalogPermissionRule } from '../types';
|
||||
import { createCatalogPermissionRule } from './util';
|
||||
|
||||
/**
|
||||
* A {@link CatalogPermissionRule} which filters for entities with a specified
|
||||
* kind.
|
||||
* A catalog {@link @backstage/plugin-permission-node#PermissionRule} which
|
||||
* filters for entities with a specified kind.
|
||||
* @public
|
||||
*/
|
||||
export const isEntityKind: CatalogPermissionRule = {
|
||||
export const isEntityKind = createCatalogPermissionRule({
|
||||
name: 'IS_ENTITY_KIND',
|
||||
description: 'Allow entities with the specified kind',
|
||||
apply(resource: Entity, kinds: string[]) {
|
||||
@@ -35,4 +35,4 @@ export const isEntityKind: CatalogPermissionRule = {
|
||||
values: kinds.map(kind => kind.toLocaleLowerCase('en-US')),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -19,15 +19,14 @@ import {
|
||||
RELATION_OWNED_BY,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { EntitiesSearchFilter } from '../../catalog/types';
|
||||
import { CatalogPermissionRule } from '../types';
|
||||
import { createCatalogPermissionRule } from './util';
|
||||
|
||||
/**
|
||||
* A {@link CatalogPermissionRule} which filters for entities with a specified
|
||||
* owner.
|
||||
* A catalog {@link @backstage/plugin-permission-node#PermissionRule} which
|
||||
* filters for entities with a specified owner.
|
||||
* @public
|
||||
*/
|
||||
export const isEntityOwner: CatalogPermissionRule = {
|
||||
export const isEntityOwner = createCatalogPermissionRule({
|
||||
name: 'IS_ENTITY_OWNER',
|
||||
description: 'Allow entities owned by the current user',
|
||||
apply: (resource: Entity, claims: string[]) => {
|
||||
@@ -39,8 +38,8 @@ export const isEntityOwner: CatalogPermissionRule = {
|
||||
.filter(relation => relation.type === RELATION_OWNED_BY)
|
||||
.some(relation => claims.includes(stringifyEntityRef(relation.target)));
|
||||
},
|
||||
toQuery: (claims: string[]): EntitiesSearchFilter => ({
|
||||
toQuery: (claims: string[]) => ({
|
||||
key: 'relations.ownedBy',
|
||||
values: claims,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
+9
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -13,18 +13,19 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { PermissionRule } from '@backstage/plugin-permission-node';
|
||||
import { EntitiesSearchFilter } from '../catalog/types';
|
||||
import { makeCreatePermissionRule } from '@backstage/plugin-permission-node';
|
||||
import { EntitiesSearchFilter } from '../../catalog/types';
|
||||
|
||||
/**
|
||||
* A conditional rule that can be used to filter catalog entities for an
|
||||
* authorization request. See
|
||||
* {@link @backstage/plugin-permission-node#PermissionRule} for more details.
|
||||
* Helper function for creating correctly-typed
|
||||
* {@link @backstage/plugin-permission-node#PermissionRule}s for the
|
||||
* catalog-backend.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type CatalogPermissionRule = PermissionRule<
|
||||
export const createCatalogPermissionRule = makeCreatePermissionRule<
|
||||
Entity,
|
||||
EntitiesSearchFilter
|
||||
>;
|
||||
>();
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { NotAllowedError } from '@backstage/errors';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
import { AuthorizedRefreshService } from './AuthorizedRefreshService';
|
||||
|
||||
describe('AuthorizedRefreshService', () => {
|
||||
const refreshService = {
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
const permissionApi = {
|
||||
authorize: jest.fn(),
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('throws AuthorizationError on deny', async () => {
|
||||
permissionApi.authorize.mockResolvedValueOnce([
|
||||
{
|
||||
result: AuthorizeResult.DENY,
|
||||
},
|
||||
]);
|
||||
const authorizedService = new AuthorizedRefreshService(
|
||||
refreshService,
|
||||
permissionApi as unknown as ServerPermissionClient,
|
||||
);
|
||||
|
||||
await expect(() =>
|
||||
authorizedService.refresh({
|
||||
entityRef: 'some entity ref',
|
||||
authorizationToken: 'some auth token',
|
||||
}),
|
||||
).rejects.toThrowError(NotAllowedError);
|
||||
});
|
||||
|
||||
it('calls refresh on allow', async () => {
|
||||
permissionApi.authorize.mockResolvedValueOnce([
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
]);
|
||||
const authorizedService = new AuthorizedRefreshService(
|
||||
refreshService,
|
||||
permissionApi as unknown as ServerPermissionClient,
|
||||
);
|
||||
|
||||
const options = {
|
||||
entityRef: 'some entity ref',
|
||||
authorizationToken: 'some auth token',
|
||||
};
|
||||
await authorizedService.refresh(options);
|
||||
|
||||
expect(refreshService.refresh).toHaveBeenCalledWith(options);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { NotAllowedError } from '@backstage/errors';
|
||||
import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PermissionAuthorizer,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { RefreshOptions, RefreshService } from './types';
|
||||
|
||||
export class AuthorizedRefreshService implements RefreshService {
|
||||
constructor(
|
||||
private readonly service: RefreshService,
|
||||
private readonly permissionApi: PermissionAuthorizer,
|
||||
) {}
|
||||
|
||||
async refresh(options: RefreshOptions) {
|
||||
const authorizeResponse = (
|
||||
await this.permissionApi.authorize(
|
||||
[
|
||||
{
|
||||
permission: catalogEntityRefreshPermission,
|
||||
resourceRef: options.entityRef,
|
||||
},
|
||||
],
|
||||
{ token: options.authorizationToken },
|
||||
)
|
||||
)[0];
|
||||
if (authorizeResponse.result !== AuthorizeResult.ALLOW) {
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
await this.service.refresh(options);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common';
|
||||
import {
|
||||
DefaultNamespaceEntityPolicy,
|
||||
Entity,
|
||||
EntityPolicies,
|
||||
EntityPolicy,
|
||||
FieldFormatEntityPolicy,
|
||||
@@ -29,7 +30,7 @@ import { ScmIntegrations } from '@backstage/integration';
|
||||
import { createHash } from 'crypto';
|
||||
import { Router } from 'express';
|
||||
import lodash from 'lodash';
|
||||
import { EntitiesCatalog } from '../catalog';
|
||||
import { EntitiesCatalog, EntitiesSearchFilter } from '../catalog';
|
||||
import {
|
||||
DatabaseLocationsCatalog,
|
||||
LocationsCatalog,
|
||||
@@ -77,19 +78,22 @@ import {
|
||||
} from '../processing/refresh';
|
||||
import { createNextRouter } from './NextRouter';
|
||||
import { DefaultRefreshService } from './DefaultRefreshService';
|
||||
import { AuthorizedRefreshService } from './AuthorizedRefreshService';
|
||||
import { DefaultCatalogRulesEnforcer } from '../ingestion/CatalogRules';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Logger } from 'winston';
|
||||
import { LocationService } from './types';
|
||||
import { connectEntityProviders } from '../processing/connectEntityProviders';
|
||||
import { CatalogPermissionRule } from '../permissions/types';
|
||||
import { permissionRules as catalogPermissionRules } from '../permissions/rules';
|
||||
import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
|
||||
import { PermissionRule } from '@backstage/plugin-permission-node';
|
||||
|
||||
export type CatalogEnvironment = {
|
||||
logger: Logger;
|
||||
database: PluginDatabaseManager;
|
||||
config: Config;
|
||||
reader: UrlReader;
|
||||
permissions: PermissionAuthorizer;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -127,7 +131,11 @@ export class NextCatalogBuilder {
|
||||
maxSeconds: 150,
|
||||
});
|
||||
private locationAnalyzer: LocationAnalyzer | undefined = undefined;
|
||||
private permissionRules: CatalogPermissionRule[];
|
||||
private permissionRules: PermissionRule<
|
||||
Entity,
|
||||
EntitiesSearchFilter,
|
||||
unknown[]
|
||||
>[];
|
||||
|
||||
constructor(env: CatalogEnvironment) {
|
||||
this.env = env;
|
||||
@@ -328,7 +336,13 @@ export class NextCatalogBuilder {
|
||||
*
|
||||
* @param permissionRules - Additional permission rules
|
||||
*/
|
||||
addPermissionRules(...permissionRules: CatalogPermissionRule[]) {
|
||||
addPermissionRules(
|
||||
...permissionRules: PermissionRule<
|
||||
Entity,
|
||||
EntitiesSearchFilter,
|
||||
unknown[]
|
||||
>[]
|
||||
) {
|
||||
this.permissionRules.push(...permissionRules);
|
||||
}
|
||||
|
||||
@@ -344,7 +358,7 @@ export class NextCatalogBuilder {
|
||||
locationService: LocationService;
|
||||
router: Router;
|
||||
}> {
|
||||
const { config, database, logger } = this.env;
|
||||
const { config, database, logger, permissions } = this.env;
|
||||
|
||||
const policy = this.buildEntityPolicy();
|
||||
const processors = this.buildProcessors();
|
||||
@@ -398,9 +412,10 @@ export class NextCatalogBuilder {
|
||||
locationStore,
|
||||
orchestrator,
|
||||
);
|
||||
const refreshService = new DefaultRefreshService({
|
||||
database: processingDatabase,
|
||||
});
|
||||
const refreshService = new AuthorizedRefreshService(
|
||||
new DefaultRefreshService({ database: processingDatabase }),
|
||||
permissions,
|
||||
);
|
||||
const router = await createNextRouter({
|
||||
entitiesCatalog,
|
||||
locationAnalyzer,
|
||||
|
||||
@@ -66,10 +66,12 @@ describe('createNextRouter readonly disabled', () => {
|
||||
const response = await request(app)
|
||||
.post('/refresh')
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('authorization', 'Bearer someauthtoken')
|
||||
.send({ entityRef: 'Component/default:foo' });
|
||||
expect(response.status).toBe(200);
|
||||
expect(refreshService.refresh).toHaveBeenCalledWith({
|
||||
entityRef: 'Component/default:foo',
|
||||
authorizationToken: 'someauthtoken',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,14 +25,16 @@ import {
|
||||
import { Config } from '@backstage/config';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common';
|
||||
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
import {
|
||||
createPermissionIntegrationRouter,
|
||||
PermissionRule,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import yn from 'yn';
|
||||
import { EntitiesCatalog } from '../catalog';
|
||||
import { EntitiesCatalog, EntitiesSearchFilter } from '../catalog';
|
||||
import { LocationAnalyzer } from '../ingestion/types';
|
||||
import { CatalogPermissionRule } from '../permissions/types';
|
||||
import {
|
||||
basicEntityFilter,
|
||||
parseEntityFilterParams,
|
||||
@@ -40,7 +42,7 @@ import {
|
||||
parseEntityTransformParams,
|
||||
} from '../service/request';
|
||||
import { disallowReadonlyMode, validateRequestBody } from '../service/util';
|
||||
import { RefreshService, RefreshOptions, LocationService } from './types';
|
||||
import { RefreshOptions, LocationService, RefreshService } from './types';
|
||||
|
||||
export interface NextRouterOptions {
|
||||
entitiesCatalog?: EntitiesCatalog;
|
||||
@@ -49,7 +51,7 @@ export interface NextRouterOptions {
|
||||
refreshService?: RefreshService;
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
permissionRules?: CatalogPermissionRule[];
|
||||
permissionRules?: PermissionRule<Entity, EntitiesSearchFilter, unknown[]>[];
|
||||
}
|
||||
|
||||
export async function createNextRouter(
|
||||
@@ -77,6 +79,10 @@ export async function createNextRouter(
|
||||
if (refreshService) {
|
||||
router.post('/refresh', async (req, res) => {
|
||||
const refreshOptions: RefreshOptions = req.body;
|
||||
refreshOptions.authorizationToken = getBearerToken(
|
||||
req.header('authorization'),
|
||||
);
|
||||
|
||||
await refreshService.refresh(refreshOptions);
|
||||
res.status(200).send();
|
||||
});
|
||||
@@ -214,3 +220,13 @@ async function getEntityResource(
|
||||
|
||||
return entities[0];
|
||||
}
|
||||
|
||||
function getBearerToken(
|
||||
authorizationHeader: string | undefined,
|
||||
): string | undefined {
|
||||
if (typeof authorizationHeader !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
|
||||
return matches?.[1];
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
import {
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
ServerTokenManager,
|
||||
SingleHostDiscovery,
|
||||
UrlReaders,
|
||||
useHotMemoize,
|
||||
} from '@backstage/backend-common';
|
||||
@@ -25,6 +27,7 @@ import { Logger } from 'winston';
|
||||
import { DatabaseManager } from '../legacy/database';
|
||||
import { CatalogBuilder } from '../legacy/service/CatalogBuilder';
|
||||
import { createRouter } from '../legacy/service';
|
||||
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
@@ -42,6 +45,12 @@ export async function startStandaloneServer(
|
||||
const db = useHotMemoize(module, () =>
|
||||
DatabaseManager.createInMemoryDatabaseConnection(),
|
||||
);
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
const tokenManager = ServerTokenManager.fromConfig(config, { logger });
|
||||
const permissions = ServerPermissionClient.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager,
|
||||
});
|
||||
|
||||
logger.debug('Creating application...');
|
||||
const builder = new CatalogBuilder({
|
||||
@@ -49,6 +58,7 @@ export async function startStandaloneServer(
|
||||
database: { getClient: () => db },
|
||||
config,
|
||||
reader,
|
||||
permissions,
|
||||
});
|
||||
const { entitiesCatalog, locationsCatalog, higherOrderOperation } =
|
||||
await builder.build();
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface LocationService {
|
||||
export type RefreshOptions = {
|
||||
/** The reference to a single entity that should be refreshed */
|
||||
entityRef: string;
|
||||
authorizationToken?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -95,6 +95,22 @@ export const createPermissionIntegrationRouter: <TResource>(options: {
|
||||
getResource: (resourceRef: string) => Promise<TResource | undefined>;
|
||||
}) => Router;
|
||||
|
||||
// @public
|
||||
export const createPermissionRule: <
|
||||
TResource,
|
||||
TQuery,
|
||||
TParams extends unknown[],
|
||||
>(
|
||||
rule: PermissionRule<TResource, TQuery, TParams>,
|
||||
) => PermissionRule<TResource, TQuery, TParams>;
|
||||
|
||||
// @public
|
||||
export const makeCreatePermissionRule: <TResource, TQuery>() => <
|
||||
TParams extends unknown[],
|
||||
>(
|
||||
rule: PermissionRule<TResource, TQuery, TParams>,
|
||||
) => PermissionRule<TResource, TQuery, TParams>;
|
||||
|
||||
// @public
|
||||
export interface PermissionPolicy {
|
||||
// (undocumented)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PermissionRule } from '../types';
|
||||
|
||||
/**
|
||||
* Helper function to ensure that {@link PermissionRule} definitions are typed correctly.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const createPermissionRule = <
|
||||
TResource,
|
||||
TQuery,
|
||||
TParams extends unknown[],
|
||||
>(
|
||||
rule: PermissionRule<TResource, TQuery, TParams>,
|
||||
) => rule;
|
||||
|
||||
/**
|
||||
* Helper for making plugin-specific createPermissionRule functions, that have
|
||||
* the TResource and TQuery type parameters populated but infer the params from
|
||||
* the supplied rule. This helps ensure that rules created for this plugin use
|
||||
* consistent types for the resource and query.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const makeCreatePermissionRule =
|
||||
<TResource, TQuery>() =>
|
||||
<TParams extends unknown[]>(
|
||||
rule: PermissionRule<TResource, TQuery, TParams>,
|
||||
) =>
|
||||
createPermissionRule(rule);
|
||||
@@ -18,3 +18,4 @@ export * from './createConditionFactory';
|
||||
export * from './createConditionExports';
|
||||
export * from './createConditionTransformer';
|
||||
export * from './createPermissionIntegrationRouter';
|
||||
export * from './createPermissionRule';
|
||||
|
||||
@@ -734,7 +734,7 @@ async function main() {
|
||||
if (!selectedPackageDirs && !isCiBuild && !isDocsBuild) {
|
||||
console.log('');
|
||||
console.log(
|
||||
'TIP: You can generate changesets for select packages by passing package paths:',
|
||||
'TIP: You can generate api-reports for select packages by passing package paths:',
|
||||
);
|
||||
console.log('');
|
||||
console.log(
|
||||
|
||||
@@ -4745,9 +4745,9 @@
|
||||
integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==
|
||||
|
||||
"@microsoft/microsoft-graph-types@^2.6.0":
|
||||
version "2.8.0"
|
||||
resolved "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.8.0.tgz#c3b538f99028e8609c5ebf95a494318a8f3d9201"
|
||||
integrity sha512-NDgLn9IhYD/+nCeeGAi1JM7xTFqaM6rkXfLfiC1xvXy48BGBUrAf8fNFq5fkzBvGY8HfjzdPIkrJkfvLL+rzDQ==
|
||||
version "2.11.0"
|
||||
resolved "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.11.0.tgz#0e1d3a0795855fc726e08836b1d3c4a72a8bcd00"
|
||||
integrity sha512-v4Wuxp+kbcxeJGmb2UHbcukNr05XItFYXL+U3ReignI3Vl8tp1vfq0hkqP35Fun2QpqHJiu8Rkxj1MUF8d82ag==
|
||||
|
||||
"@microsoft/tsdoc-config@~0.15.2":
|
||||
version "0.15.2"
|
||||
@@ -5209,19 +5209,19 @@
|
||||
resolved "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-2.0.0.tgz#1108b9ea661ca6c81e4a8bfa63a09eb27d5bc2db"
|
||||
integrity sha512-35cfQ4YWlnZnmZKmIxlGPUPLtbkF8lr/A/1Sk1eC0ddLMwQN06dOuLc+dI3YLQS+T+MoNt3DIQ0NynwgKPilig==
|
||||
|
||||
"@octokit/webhooks-types@4.15.0":
|
||||
version "4.15.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-4.15.0.tgz#1158cba6578237d60957a37963a4a05654f5668b"
|
||||
integrity sha512-s9LgKsUzq/JH3PWDjaD/m1DIlC/QWgBWbmXVqjdxJXJQBA67KZrLWjStVlYPf0mWlVZ1MOKphDyHiOGCbs0+Kg==
|
||||
"@octokit/webhooks-types@5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-5.2.0.tgz#9d1d451f37460107409c81cab04dd473108abb02"
|
||||
integrity sha512-OZhKy1w8/GF4GWtdiJc+o8sloWAHRueGB78FWFLZnueK7EHV9MzDVr4weJZMflJwMK4uuYLzcnJVnAoy3yB35g==
|
||||
|
||||
"@octokit/webhooks@^9.14.1":
|
||||
version "9.18.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-9.18.0.tgz#19cc70e1ef281e33d830ea23e8011d25d8051f7f"
|
||||
integrity sha512-N2hP7vCouKk9UWZxvqgWTPbp34i6g9Om/jk+TZeZ5Z+VsKjXvGtONlEd9H8DM1yOeEC+ARDpfhraX6UsK5tesQ==
|
||||
version "9.22.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-9.22.0.tgz#07a36a10358d39c1870758fae2b1ad3c24ca578d"
|
||||
integrity sha512-wUd7nGfDRHG6xkz311djmq6lIB2tQ+r94SNkyv9o0bQhOsrkwH8fQCM7uVsbpkGUU2lqCYsVoa8z/UC9HJgRaw==
|
||||
dependencies:
|
||||
"@octokit/request-error" "^2.0.2"
|
||||
"@octokit/webhooks-methods" "^2.0.0"
|
||||
"@octokit/webhooks-types" "4.15.0"
|
||||
"@octokit/webhooks-types" "5.2.0"
|
||||
aggregate-error "^3.1.0"
|
||||
|
||||
"@open-draft/until@^1.0.3":
|
||||
|
||||
Reference in New Issue
Block a user