Skip to content

Commit

Permalink
Merge pull request #1103 from StuartApp/custom-user-transformer
Browse files Browse the repository at this point in the history
Add option to provide custom user transformer
  • Loading branch information
kissmikijr authored Sep 27, 2023
2 parents f2d9f43 + aadd29e commit bfebbbf
Show file tree
Hide file tree
Showing 11 changed files with 548 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-worms-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@roadiehq/catalog-backend-module-okta': minor
---

Adds option to provide custom user transformer for user and org providers
219 changes: 219 additions & 0 deletions plugins/backend/catalog-backend-module-okta/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,83 @@ export default async function createPlugin(
}
```

In case you want to customize the emitted entities, the provider allows to pass custom transformers for users and groups by providing `userTransformer` and `groupTransformer`.

1. Create a transformer:

```typescript
import { GroupNamingStrategy } from '@roadiehq/catalog-backend-module-okta';
import { GroupEntity } from '@backstage/catalog-model';
import { Group } from '@okta/okta-sdk-nodejs';
function myGroupTransformer(
group: Group,
namingStrategy: GroupNamingStrategy,
parentGroup: Group | undefined,
options: {
annotations: Record<string, string>;
members: string[];
},
): GroupEntity {
// Enrich it with your logic
const groupEntity: GroupEntity = {
kind: 'Group',
apiVersion: 'backstage.io/v1alpha1',
metadata: {
annotations: {
...options.annotations,
},
name: namingStrategy(group),
title: group.profile.name,
title: group.profile.description || group.profile.name,
description: group.profile.description || '',
},
spec: {
members: options.members,
type: 'group',
children: [],
},
};
if (parentGroup) {
groupEntity.spec.parent = namingStrategy(parentGroup);
}
return groupEntity;
}
```

2. Configure the provider with the transformer:

```typescript
import { OktaOrgEntityProvider } from '@roadiehq/catalog-backend-module-okta';
import { myGroupTransformer } from './myGroupTransformer';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
const orgProvider = OktaOrgEntityProvider.fromConfig(env.config, {
logger: env.logger,
userNamingStrategy: 'strip-domain-email',
groupNamingStrategy: 'kebab-case-name',
groupTransformer: myGroupTransformer,
});
builder.addEntityProvider(orgProvider);
const { processingEngine, router } = await builder.build();
orgProvider.run();
await processingEngine.start();
// ...
return router;
}
```

## Load Users and Groups Separately

### OktaUserEntityProvider
Expand All @@ -165,6 +242,71 @@ export const customUserNamingStrategy: UserNamingStrategy = user =>
user.profile.customField;
```

In case you want to customize the emitted entities, the provider allows to pass custom transformer by providing `userTransformer`.

1. Create a transformer:

```typescript
import { UserEntity } from '@backstage/catalog-model';
import { User } from '@okta/okta-sdk-nodejs';
import { UserNamingStrategy } from '@roadiehq/catalog-backend-module-okta';
function myUserTransformer(
user: User,
namingStrategy: UserNamingStrategy,
options: { annotations: Record<string, string> },
): UserEntity {
// Enrich it with your logic
return {
kind: 'User',
apiVersion: 'backstage.io/v1alpha1',
metadata: {
annotations: { ...options.annotations },
name: namingStrategy(user),
title: user.profile.email,
},
spec: {
profile: {
displayName: user.profile.displayName,
email: user.profile.email,
},
memberOf: [],
},
};
}
```

2. Configure the provider with the transformer:

```typescript
import { OktaUserEntityProvider } from '@roadiehq/catalog-backend-module-okta';
import { myUserTransformer } from './myUserTransformer';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
const userProvider = OktaUserEntityProvider.fromConfig(env.config, {
logger: env.logger,
namingStrategy: 'strip-domain-email',
userTransformer: myUserTransformer,
});
builder.addEntityProvider(userProvider);
const { processingEngine, router } = await builder.build();
userProvider.run();
await processingEngine.start();
// ...
return router;
}
```

### OktaGroupEntityProvider

You can configure the provider with different naming strategies. The configured strategy will be used to generate the discovered entities `metadata.name` field. The currently supported strategies are the following:
Expand Down Expand Up @@ -283,3 +425,80 @@ export default async function createPlugin(
return router;
}
```

In case you want to customize the emitted entities, the provider allows to pass custom transformer by providing `groupTransformer`.

1. Create a transformer:

```typescript
import { GroupNamingStrategy } from '@roadiehq/catalog-backend-module-okta';
import { GroupEntity } from '@backstage/catalog-model';
import { Group } from '@okta/okta-sdk-nodejs';
function myGroupTransformer(
group: Group,
namingStrategy: GroupNamingStrategy,
parentGroup: Group | undefined,
options: {
annotations: Record<string, string>;
members: string[];
},
): GroupEntity {
// Enrich it with your logic
const groupEntity: GroupEntity = {
kind: 'Group',
apiVersion: 'backstage.io/v1alpha1',
metadata: {
annotations: {
...options.annotations,
},
name: namingStrategy(group),
title: group.profile.name,
title: group.profile.description || group.profile.name,
description: group.profile.description || '',
},
spec: {
members: options.members,
type: 'group',
children: [],
},
};
if (parentGroup) {
groupEntity.spec.parent = namingStrategy(parentGroup);
}
return groupEntity;
}
```

2. Configure the provider with the transformer:

```typescript
import { OktaGroupEntityProvider } from '@roadiehq/catalog-backend-module-okta';
import { myGroupTransformer } from './myGroupTransformer';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
const groupProvider = OktaGroupEntityProvider.fromConfig(env.config, {
logger: env.logger,
userNamingStrategy: 'strip-domain-email',
groupNamingStrategy: 'kebab-case-name',
groupTransformer: myGroupTransformer,
});
builder.addEntityProvider(groupProvider);
const { processingEngine, router } = await builder.build();
groupProvider.run();
await processingEngine.start();
// ...
return router;
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ describe('OktaGroupProvider', () => {
description: 'Everyone in the company',
org_id: '1234',
parent_org_id: '1234',
displayName: 'Display name 1',
},
listUsers: () => {
return new MockOktaCollection([
Expand All @@ -160,6 +161,7 @@ describe('OktaGroupProvider', () => {
description: null,
org_id: '1235',
parent_org_id: '1234',
displayName: 'Display name 2',
},
listUsers: () => {
return new MockOktaCollection([
Expand Down Expand Up @@ -452,5 +454,56 @@ describe('OktaGroupProvider', () => {
]),
});
});

it('uses custom group transformer', async () => {
const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
refresh: jest.fn(),
};
const provider = OktaGroupEntityProvider.fromConfig(config, {
logger,
groupTransformer: (group, namingStrategy, options, parentGroup) => ({
kind: 'Group',
apiVersion: 'backstage.io/v1alpha1',
metadata: {
annotations: { ...options.annotations },
name: namingStrategy(group),
title: group.profile.name,
description: group.profile.description || '',
},
spec: {
profile: {
displayName: group.profile.displayName as string,
},
members: options.members,
type: 'group',
children: [],
parent: parentGroup ? namingStrategy(parentGroup) : '',
},
}),
});
provider.connect(entityProviderConnection);
await provider.run();
expect(entityProviderConnection.applyMutation).toBeCalledWith({
type: 'full',
entities: expect.arrayContaining([
expect.objectContaining({
entity: expect.objectContaining({
kind: 'Group',
metadata: expect.objectContaining({
name: 'asdfwefwefwef',
description: 'Everyone in the company',
}),
spec: expect.objectContaining({
members: ['asdfwefwefwef'],
profile: expect.objectContaining({
displayName: 'Display name 1',
}),
}),
}),
}),
]),
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,20 @@ import {
userNamingStrategyFactory,
} from './userNamingStrategies';
import { AccountConfig } from '../types';
import { groupEntityFromOktaGroup } from './groupEntityFromOktaGroup';
import { groupEntityFromOktaGroup as defaultGroupEntityFromOktaGroup } from './groupEntityFromOktaGroup';
import { getAccountConfig } from './accountConfig';
import { isError } from '@backstage/errors';
import { getOktaGroups } from './getOktaGroups';
import { getParentGroup } from './getParentGroup';
import { OktaGroupEntityTransformer } from './types';

/**
* Provides entities from Okta Group service.
*/
export class OktaGroupEntityProvider extends OktaEntityProvider {
private readonly namingStrategy: GroupNamingStrategy;
private readonly userNamingStrategy: UserNamingStrategy;
private readonly groupEntityFromOktaGroup: OktaGroupEntityTransformer;
private readonly groupFilter: string | undefined;
private readonly orgUrl: string;
private readonly customAttributesToAnnotationAllowlist: string[];
Expand All @@ -52,6 +54,7 @@ export class OktaGroupEntityProvider extends OktaEntityProvider {
logger: winston.Logger;
namingStrategy?: GroupNamingStrategies | GroupNamingStrategy;
userNamingStrategy?: UserNamingStrategies | UserNamingStrategy;
groupTransformer?: OktaGroupEntityTransformer;
customAttributesToAnnotationAllowlist?: string[];
/*
* @deprecated, please use hierarchyConfig.parentKey
Expand Down Expand Up @@ -85,13 +88,16 @@ export class OktaGroupEntityProvider extends OktaEntityProvider {
parentKey: string;
key?: string;
};
groupTransformer?: OktaGroupEntityTransformer;
},
) {
super([accountConfig], options);
this.namingStrategy = groupNamingStrategyFactory(options.namingStrategy);
this.userNamingStrategy = userNamingStrategyFactory(
options.userNamingStrategy,
);
this.groupEntityFromOktaGroup =
options?.groupTransformer || defaultGroupEntityFromOktaGroup;
this.orgUrl = accountConfig.orgUrl;
this.groupFilter = accountConfig.groupFilter;
this.customAttributesToAnnotationAllowlist =
Expand Down Expand Up @@ -160,14 +166,14 @@ export class OktaGroupEntityProvider extends OktaEntityProvider {
...profileAnnotations,
};
try {
const groupEntity = groupEntityFromOktaGroup(
const groupEntity = this.groupEntityFromOktaGroup(
group,
this.namingStrategy,
parentGroup,
{
annotations,
members,
},
parentGroup,
);
groupResources.push(groupEntity);
} catch (e) {
Expand Down
Loading

0 comments on commit bfebbbf

Please sign in to comment.