fix(docs): Replace instances of UrlReader with UrlReaderService
Also update the service names in threat-model.md Signed-off-by: Łukasz Jernaś <lukasz.jernas@allegro.com>
This commit is contained in:
@@ -77,28 +77,30 @@ yarn new --select backend-module --option id=catalog
|
||||
|
||||
The class will have this basic structure:
|
||||
|
||||
```ts
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
```ts title="plugins/catalog-backend-module-frobs/src/FrobsProvider.ts"
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
SchedulerServiceTaskRunner,
|
||||
UrlReaderService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/**
|
||||
* Provides entities from fictional frobs service.
|
||||
*/
|
||||
export class FrobsProvider implements EntityProvider {
|
||||
private readonly env: string;
|
||||
private readonly reader: UrlReader;
|
||||
private readonly reader: UrlReaderService;
|
||||
private connection?: EntityProviderConnection;
|
||||
private taskRunner: SchedulerServiceTaskRunner;
|
||||
|
||||
/** [1] */
|
||||
constructor(
|
||||
env: string,
|
||||
reader: UrlReader,
|
||||
reader: UrlReaderService,
|
||||
taskRunner: SchedulerServiceTaskRunner,
|
||||
) {
|
||||
this.env = env;
|
||||
@@ -114,7 +116,7 @@ export class FrobsProvider implements EntityProvider {
|
||||
/** [3] */
|
||||
async connect(connection: EntityProviderConnection): Promise<void> {
|
||||
this.connection = connection;
|
||||
this.taskRunner.run({
|
||||
await this.taskRunner.run({
|
||||
id: this.getProviderName(),
|
||||
fn: async () => {
|
||||
await this.run();
|
||||
@@ -131,7 +133,7 @@ export class FrobsProvider implements EntityProvider {
|
||||
const response = await this.reader.readUrl(
|
||||
`https://frobs-${this.env}.example.com/data`,
|
||||
);
|
||||
const data = JSON.parse(await response.buffer()).toString();
|
||||
const data = JSON.parse((await response.buffer()).toString());
|
||||
|
||||
/** [5] */
|
||||
const entities: Entity[] = frobsToEntities(data);
|
||||
@@ -200,7 +202,7 @@ bucket is accessible.
|
||||
There are two different types of mutation.
|
||||
|
||||
The first is `'full'`, which means to figuratively throw away the contents of
|
||||
the bucket and replacing it with all of the new contents specified. Under the
|
||||
the bucket and replacing it with all the new contents specified. Under the
|
||||
hood, this is actually implemented through a highly efficient delta mechanism
|
||||
for performance reasons, since it is common that the difference from one run to
|
||||
the other is actually very small. This strategy is convenient for providers that
|
||||
@@ -436,66 +438,66 @@ We create a basic entity provider as shown above. In the example below we might
|
||||
import {
|
||||
ANNOTATION_LOCATION,
|
||||
ANNOTATION_ORIGIN_LOCATION,
|
||||
} from '@backstage/catalog-model'
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
} from '@backstage/plugin-catalog-backend'
|
||||
import { WebClient } from '@slack/web-api'
|
||||
import {kebabCase} from 'lodash'
|
||||
} from '@backstage/plugin-catalog-backend';
|
||||
import { WebClient } from '@slack/web-api';
|
||||
import { kebabCase } from 'lodash';
|
||||
|
||||
interface Staff {
|
||||
displayName: string
|
||||
slackUserId: string
|
||||
jobTitle: string
|
||||
photoUrl: string
|
||||
address: string
|
||||
email:string
|
||||
displayName: string;
|
||||
slackUserId: string;
|
||||
jobTitle: string;
|
||||
photoUrl: string;
|
||||
address: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export class UserEntityProvider implements EntityProvider {
|
||||
private readonly getStaffUrl: string
|
||||
protected readonly slackTeam: string
|
||||
protected readonly slackToken: string
|
||||
protected connection?: EntityProviderConnection
|
||||
private readonly getStaffUrl: string;
|
||||
protected readonly slackTeam: string;
|
||||
protected readonly slackToken: string;
|
||||
protected connection?: EntityProviderConnection;
|
||||
|
||||
static fromConfig(config: Config, options: { logger: Logger }) {
|
||||
const getStaffUrl = config.getString('staff.url')
|
||||
const slackToken = config.getString('slack.token')
|
||||
const slackTeam = config.getString('slack.team')
|
||||
const getStaffUrl = config.getString('staff.url');
|
||||
const slackToken = config.getString('slack.token');
|
||||
const slackTeam = config.getString('slack.team');
|
||||
return new UserEntityProvider({
|
||||
...options,
|
||||
getStaffUrl,
|
||||
slackToken,
|
||||
slackTeam,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private constructor(options: {
|
||||
getStaffUrl: string
|
||||
slackToken: string
|
||||
slackTeam: string
|
||||
getStaffUrl: string;
|
||||
slackToken: string;
|
||||
slackTeam: string;
|
||||
}) {
|
||||
this.getStaffUrl = options.getStaffUrl
|
||||
this.slackToken = options.slackToken
|
||||
this.slackTeam = options.slackTeam
|
||||
this.getStaffUrl = options.getStaffUrl;
|
||||
this.slackToken = options.slackToken;
|
||||
this.slackTeam = options.slackTeam;
|
||||
}
|
||||
|
||||
async getAllStaff(): Promise<Staff[]>{
|
||||
await return axios.get(this.getStaffUrl)
|
||||
async getAllStaff(): Promise<Staff[]> {
|
||||
return await axios.get(this.getStaffUrl);
|
||||
}
|
||||
|
||||
public async connect(connection: EntityProviderConnection): Promise<void> {
|
||||
this.connection = connection
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
if (!this.connection) {
|
||||
throw new Error('User Connection Not initialized')
|
||||
throw new Error('User Connection Not initialized');
|
||||
}
|
||||
|
||||
const userResources: UserEntity[] = []
|
||||
const staff = await this.getAllStaff()
|
||||
const userResources: UserEntity[] = [];
|
||||
const staff = await this.getAllStaff();
|
||||
|
||||
for (const user of staff) {
|
||||
// we can add any links here in this case it would be adding a slack link to the users so you can directly slack them.
|
||||
@@ -508,7 +510,7 @@ export class UserEntityProvider implements EntityProvider {
|
||||
icon: 'message',
|
||||
},
|
||||
]
|
||||
: undefined
|
||||
: undefined;
|
||||
const userEntity: UserEntity = {
|
||||
kind: 'User',
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
@@ -531,21 +533,20 @@ export class UserEntityProvider implements EntityProvider {
|
||||
},
|
||||
memberOf: [],
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
userResources.push(userEntity)
|
||||
userResources.push(userEntity);
|
||||
}
|
||||
|
||||
await this.connection.applyMutation({
|
||||
type: 'full',
|
||||
entities: userResources.map((entity) => ({
|
||||
entities: userResources.map(entity => ({
|
||||
entity,
|
||||
locationKey: 'hr-user-https://www.hrurl.com/',
|
||||
})),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Custom Processors
|
||||
@@ -596,7 +597,7 @@ startup. They are at the heart of all catalog logic, and have the ability to
|
||||
read the contents of locations, modify in-flight entities that were read out of
|
||||
a location, perform validation, and more. The catalog comes with a set of
|
||||
builtin processors, that have the ability to read from a list of well known
|
||||
location types, to perform the basic processing needs, etc, but more can be
|
||||
location types, to perform the basic processing needs, etc., but more can be
|
||||
added by the organization that adopts Backstage.
|
||||
|
||||
We will now show the process of creating a new processor and location type,
|
||||
@@ -655,18 +656,18 @@ yarn new --select backend-module --option id=catalog
|
||||
The class will have this basic structure:
|
||||
|
||||
```ts
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import {
|
||||
processingResult,
|
||||
CatalogProcessor,
|
||||
CatalogProcessorEmit,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import { UrlReaderService } from '@backstage/backend-plugin-api';
|
||||
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-common';
|
||||
|
||||
// A processor that reads from the fictional System-X
|
||||
export class SystemXReaderProcessor implements CatalogProcessor {
|
||||
constructor(private readonly reader: UrlReader) {}
|
||||
constructor(private readonly reader: UrlReaderService) {}
|
||||
|
||||
getProcessorName(): string {
|
||||
return 'SystemXReaderProcessor';
|
||||
@@ -785,8 +786,8 @@ locations in GitHub. This example aims to demonstrate how to add the same
|
||||
behavior for `system-x` that we implemented earlier.
|
||||
|
||||
```ts
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { UrlReaderService } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
processingResult,
|
||||
CatalogProcessor,
|
||||
@@ -809,7 +810,7 @@ type CacheItem = {
|
||||
};
|
||||
|
||||
export class SystemXReaderProcessor implements CatalogProcessor {
|
||||
constructor(private readonly reader: UrlReader) {}
|
||||
constructor(private readonly reader: UrlReaderService) {}
|
||||
|
||||
getProcessorName() {
|
||||
// The processor name must be unique.
|
||||
@@ -852,7 +853,7 @@ export class SystemXReaderProcessor implements CatalogProcessor {
|
||||
}
|
||||
|
||||
// For this example the JSON payload is a single entity.
|
||||
const entity: Entity = JSON.parse(response.buffer.toString());
|
||||
const entity: Entity = JSON.parse(await response.buffer().toString());
|
||||
emit(processingResult.entity(location, entity));
|
||||
|
||||
// Update the cache with the new ETag and entity used for the next run.
|
||||
@@ -923,13 +924,13 @@ const makeEntityFromCustomFormatJson = (
|
||||
[ANNOTATION_LOCATION]: `${location.type}:${location.target}`,
|
||||
[ANNOTATION_ORIGIN_LOCATION]: `${location.type}:${location.target}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
spec: {
|
||||
type: component.type,
|
||||
owner: component.author,
|
||||
lifecycle: 'experimental'
|
||||
}
|
||||
}
|
||||
lifecycle: 'experimental',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const customEntityDataParser: CatalogProcessorParser = async function* ({
|
||||
@@ -962,7 +963,7 @@ export const customEntityDataParser: CatalogProcessorParser = async function* ({
|
||||
if (json.apiVersion) {
|
||||
yield processingResult.entity(location, json as Entity);
|
||||
|
||||
// let's treat this like it's our custom format instead.
|
||||
// let's treat this like it's our custom format instead.
|
||||
} else {
|
||||
yield processingResult.entity(
|
||||
location,
|
||||
@@ -979,7 +980,7 @@ export const customEntityDataParser: CatalogProcessorParser = async function* ({
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
This is a lot of code right now, as this is a pretty niche use-case, so we don't currently provide many helpers for you to be able to provide custom implementations easier or to compose together different parsers.
|
||||
@@ -1086,7 +1087,7 @@ interface Service {
|
||||
}
|
||||
```
|
||||
|
||||
These are the only 3 methods that you need to implement. `getProviderName()` is pretty self explanatory and it's identical to the `getProviderName()` method on a regular Entity Provider.
|
||||
These are the only 3 methods that you need to implement. `getProviderName()` is pretty self-explanatory and it's identical to the `getProviderName()` method on a regular Entity Provider.
|
||||
|
||||
```ts
|
||||
import { IncrementalEntityProvider } from '@backstage/plugin-catalog-backend-module-incremental-ingestion';
|
||||
@@ -1179,7 +1180,7 @@ export class MyIncrementalEntityProvider
|
||||
|
||||
async next(
|
||||
context: Context,
|
||||
cursor?: Cursor = { page: 1 },
|
||||
cursor: Cursor = { page: 1 },
|
||||
): Promise<EntityIteratorResult<Cursor>> {
|
||||
const { apiClient } = context;
|
||||
|
||||
|
||||
@@ -41,13 +41,13 @@ The built-in protection against unauthorized access does not by default include
|
||||
|
||||
## Common Backend Configuration
|
||||
|
||||
There are many common facilities that are configured centrally and available to all Backstage backend plugins. For example there is a `DatabaseManager` that provides access to a SQL database, `SchedulerService` for scheduling long-running tasks, `Logger` as a general logging facility, and `UrlReader` for reading content from external sources. These are all configured either directly in code, or within the `backend` block of the static configuration. The appropriate care needs to be taken to ensure that any secrets remain confidential and no malicious configuration is injected.
|
||||
There are many common facilities that are configured centrally and available to all Backstage backend plugins. For example there is a `DatabaseService` that provides access to a SQL database, `SchedulerService` for scheduling long-running tasks, `LoggerService` as a general logging facility, and `UrlReaderService` for reading content from external sources. These are all configured either directly in code, or within the `backend` block of the static configuration. The appropriate care needs to be taken to ensure that any secrets remain confidential and no malicious configuration is injected.
|
||||
|
||||
In a typical Backstage setup, there is no boundary between plugins that run on the same host. Likewise, there is no boundary between plugins that share the same database access. Any plugin that is running on a host that has access to the logical database of any other plugin should be considered to have full access to that other plugin. For example, even if you deploy the `auth` and `catalog` plugins on separate hosts with separate configuration and credentials, the `catalog` plugin is still considered to have full access to the `auth` plugin as long as the `catalog` plugin has access to the `auth` plugin's logical database. The only way to create a boundary between the two plugins is to deploy them in such a way that they do not have access to each others’ database. This applies to the database facility as well as any other shared resources, such as the cache.
|
||||
|
||||
The `UrlReader` facility is of particular interest for a secure Backstage configuration. In particular the `backend.reading.allow` configuration lists the hosts that you trust the backend to be able to read content from on behalf of users. It is extremely important that this list does not, for example, allow access to instance metadata endpoints of cloud providers, or other endpoints that your Backstage instance may have access to which contain sensitive information. In general it is recommended to keep the list minimal and only allow reading from required endpoints. The same concerns apply to custom implementations of the `UrlReader` interface, if you need to implement these through code.
|
||||
The `UrlReaderService` facility is of particular interest for a secure Backstage configuration. In particular the `backend.reading.allow` configuration lists the hosts that you trust the backend to be able to read content from on behalf of users. It is extremely important that this list does not, for example, allow access to instance metadata endpoints of cloud providers, or other endpoints that your Backstage instance may have access to which contain sensitive information. In general it is recommended to keep the list minimal and only allow reading from required endpoints. The same concerns apply to custom implementations of the `UrlReader` interface, if you need to implement these through code.
|
||||
|
||||
Note that the `UrlReader` system operates with a service context and is not integrated with the Backstage permission system or other external access control mechanisms. This means users of your Backstage instance may be able to read external content which that individual should not have access to. For example, the `$text` placeholder in a `catalog-info.yaml` can be used to read contents from a source such as a GitHub repository that the user does not have direct access too. If this is a concern it is recommended to either disable or replace the resolvers in the catalog placeholder processor and similar features in any other plugin.
|
||||
Note that the `UrlReaderService` system operates with a service context and is not integrated with the Backstage permission system or other external access control mechanisms. This means users of your Backstage instance may be able to read external content which that individual should not have access to. For example, the `$text` placeholder in a `catalog-info.yaml` can be used to read contents from a source such as a GitHub repository that the user does not have direct access too. If this is a concern it is recommended to either disable or replace the resolvers in the catalog placeholder processor and similar features in any other plugin.
|
||||
|
||||
## Authentication
|
||||
|
||||
|
||||
Reference in New Issue
Block a user