npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@clementblaise/catalog-backend-module-okta

v0.10.4

Published

A set of Backstage catalog providers for Okta

Downloads

3

Readme

Catalog Backend Module for Okta

This is an extension module to the plugin-catalog-backend plugin, providing entity providers to read Okta Group and User objects as Backstage Entities.

To setup the Okta providers you will need an Okta API Token

App Config

You will need to configure your okta credentials in the app-config.yaml.

Basic Config via an API Token

catalog:
  providers:
    okta:
      orgUrl: 'https://tenant.okta.com'
      token: ${OKTA_TOKEN}

Multiple Configs

You can define multiple config

catalog:
  providers:
    okta:
      org-one:
        orgUrl: 'https://tenant-one.okta.com'
        token: ${OKTA_TOKEN}
      org-two:
        orgUrl: 'https://tenant-two.okta.com'
        token: ${OKTA_TOKEN}

OAuth 2.0 Scoped Authentication

Create an OAuth app in Okta. You will need to grant it with the okta.groups.read and okta.users.read scopes as a bare minimum. In the following example the oauth.privateKey may be passed as either a string encoded PEM or stringified JWK.

catalog:
  providers:
    okta:
      orgUrl: 'https://tenant.okta.com'
      oauth:
        clientId: ${OKTA_OAUTH_CLIENT_ID},
        keyId: ${OKTA_OAUTH_KEY_ID},
        privateKey: ${OKTA_OAUTH_PRIVATE_KEY},

Note: keyId is optional but must be passed wen using a PEM as the privateKey

Filter Users and Groups

The provider allows configuring Okta search filtering for users and groups. See here for more details on what is possible: https://developer.okta.com/docs/reference/core-okta-api/#filter

catalog:
  providers:
    okta:
      orgUrl: 'https://tenant.okta.com'
      token: ${OKTA_TOKEN}
      userFilter: profile.department eq "engineering"
      groupFilter: profile.name eq "Everyone"

There are two ways that you can configure the Entity providers. You can either use the OktaOrgEntityProvider which loads both users and groups. Or you can load user or groups separately user the OktaUserEntityProvider and OktaGroupEntityProvider providers.

Load Users and Groups Together

OktaOrgEntityProvider

You can configure the provider with different naming strategies. The configured strategy will be used to generate the discovered entity's metadata.name field. The currently supported strategies are the following:

User naming stategies:

  • id (default) | User entities will be named by the user id.
  • kebab-case-email | User entities will be named by their profile email converted to kebab case.
  • strip-domain-email | User entities will be named by their profile email without the domain part.

You may also choose to implement a custom naming strategy by providing a function.

export const customUserNamingStrategy: UserNamingStrategy = user =>
  user.profile.customField;

Group naming strategies:

  • id (default) | Group entities will be named by the group id.
  • kebab-case-name | Group entities will be named by their group profile name converted to kebab case.
  • profile-name | Group entities will be named exactly as their group profile name. ⚠ The Okta field supports characters not supported as entity names in backstage. ⚠

You may also choose to implement a custom naming strategy by providing a function.

export const customGroupNamingStrategy: GroupNamingStrategy = group =>
  group.profile.customField;

Example configuration:

import { OktaOrgEntityProvider } from '@roadiehq/catalog-backend-module-okta';

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',
  })[0];

  builder.addEntityProvider(orgProvider);

  const { processingEngine, router } = await builder.build();

  orgProvider.run();

  await processingEngine.start();

  // ...

  return router;
}

You can optionally provide the ability to create a hierarchy of groups by providing hierarchyConfig.

import { OktaOrgEntityProvider } from '@roadiehq/catalog-backend-module-okta';

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',
    hierarchyConfig: {
      key: 'profile.orgId',
      parentKey: 'profile.parentOrgId',
    },
  })[0];

  builder.addEntityProvider(orgProvider);

  const { processingEngine, router } = await builder.build();

  orgProvider.run();

  await processingEngine.start();

  // ...

  return router;
}

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:
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;
}
  1. Configure the provider with the transformer:
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,
  })[0];

  builder.addEntityProvider(orgProvider);

  const { processingEngine, router } = await builder.build();

  orgProvider.run();

  await processingEngine.start();

  // ...

  return router;
}

Load Users and Groups Separately

OktaUserEntityProvider

You can configure the provider with different naming strategies. The configured strategy will be used to generate the discovered entity's metadata.name field. The currently supported strategies are the following:

  • id (default) | User entities will be named by the user id.
  • kebab-case-email | User entities will be named by their profile email converted to kebab case.
  • strip-domain-email | User entities will be named by their profile email without the domain part.

You may also choose to implement a custom naming strategy by providing a function.

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:
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: [],
    },
  };
}
  1. Configure the provider with the transformer:
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,
  })[0];

  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:

User naming stategies:

  • id (default) | User entities will be named by the user id.
  • kebab-case-email | User entities will be named by their profile email converted to kebab case.
  • strip-domain-email | User entities will be named by their profile email without the domain part.

You may also choose to implement a custom naming strategy by providing a function.

export const customUserNamingStrategy: UserNamingStrategy = user =>
  user.profile.customField;

Group naming strategies:

  • id (default) | Group entities will be named by the group id.
  • kebab-case-name | Group entities will be named by their group profile name converted to kebab case.
  • profile-name | Group entities will be named exactly as their group profile name. ⚠ The Okta field supports characters not supported as entity names in backstage. ⚠

You may also choose to implement a custom naming strategy by providing a function.

export const customGroupNamingStrategy: GroupNamingStrategy = group =>
  group.profile.customField;

Make sure you use the OktaUserEntityProvider's naming strategy for the OktaGroupEntityProvider's user naming strategy.

Example configuration:

import {
  OktaUserEntityProvider,
  OktaGroupEntityProvider,
} from '@roadiehq/catalog-backend-module-okta';

export default async function createPlugin(
  env: PluginEnvironment,
): Promise<Router> {
  const builder = await CatalogBuilder.create(env);

  const oktaConfig =
    env.config.getOptionalConfigArray('catalog.providers.okta') || [];
  const userProvider = OktaUserEntityProvider.fromConfig(oktaConfig[0], {
    logger: env.logger,
    namingStrategy: 'strip-domain-email',
  });
  const groupProvider = OktaGroupEntityProvider.fromConfig(oktaConfig[0], {
    logger: env.logger,
    userNamingStrategy: 'strip-domain-email',
    groupNamingStrategy: 'kebab-case-name',
  });

  builder.addEntityProvider(userProvider);
  builder.addEntityProvider(groupProvider);

  const { processingEngine, router } = await builder.build();

  userProvider.run();
  groupProvider.run();

  await processingEngine.start();

  // ...

  return router;
}

You can optionally provide the ability to create a hierarchy of groups by providing the hierarchyConfig.

import {
  OktaUserEntityProvider,
  OktaGroupEntityProvider,
} from '@roadiehq/catalog-backend-module-okta';

export default async function createPlugin(
  env: PluginEnvironment,
): Promise<Router> {
  const builder = await CatalogBuilder.create(env);

  const oktaConfig = env.config.getOptionalConfigArray(
    'catalog.providers.okta',
  );
  const userProvider = OktaUserEntityProvider.fromConfig(oktaConfig[0], {
    logger: env.logger,
    namingStrategy: 'strip-domain-email',
  });
  const groupProvider = OktaGroupEntityProvider.fromConfig(oktaConfig[0], {
    logger: env.logger,
    userNamingStrategy: 'strip-domain-email',
    groupNamingStrategy: 'kebab-case-name',
    hierarchyConfig: {
      key: 'profile.orgId',
      parentKey: 'profile.parentOrgId',
    },
  });

  builder.addEntityProvider(userProvider);
  builder.addEntityProvider(groupProvider);

  const { processingEngine, router } = await builder.build();

  userProvider.run();
  groupProvider.run();

  await processingEngine.start();

  // ...

  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:
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;
}
  1. Configure the provider with the transformer:
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;
}