Merge branch 'master' into mobile-sidebar

This commit is contained in:
Philipp Hugenroth
2022-01-10 10:08:00 +01:00
90 changed files with 1289 additions and 384 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/config': patch
---
The `ConfigReader#get` method now always returns a deep clone of the configuration data.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-ldap': patch
---
Make sure to avoid accidental data sharing / mutation of `set` values
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-permission-node': patch
---
Add helpers for creating PermissionRules with inferred types
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog-backend-module-ldap': patch
'@backstage/plugin-catalog-backend-module-msgraph': patch
---
Clean up API report
+60
View File
@@ -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": "...",
```
+7
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Clean up API reports
+3
View File
@@ -136,3 +136,6 @@ site
# e2e tests
cypress/cypress/*
# Possible leftover from build:api-reports
tsconfig.tmp.json
+3 -2
View File
@@ -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 |
+34
View File
@@ -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.
+23 -30
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
*/
+6 -1
View File
@@ -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,
+5 -1
View File
@@ -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.
+10 -2
View File
@@ -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;
+55 -8
View File
@@ -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({});
});
});
+3 -6
View File
@@ -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;
}
+2
View File
@@ -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": "*",
+4
View File
@@ -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,
@@ -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');
@@ -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[];
+38 -14
View File
@@ -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
+2 -1
View File
@@ -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,
}),
};
});
@@ -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;
};
/**
+16
View File
@@ -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';
+1 -1
View File
@@ -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(
+11 -11
View File
@@ -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":