Merge pull request #8809 from backstage/rugvip/depr

removed deprecations from the 2021-12-09 release
This commit is contained in:
Patrik Oldsberg
2022-01-17 14:01:34 +01:00
committed by GitHub
70 changed files with 528 additions and 518 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Updated the `SignInPage`, `ProxiedSignInPage` and `UserIdentity` implementations to match the removals of the deprecated `IdentityApi` methods and types.
+24
View File
@@ -0,0 +1,24 @@
---
'@backstage/core-plugin-api': minor
---
Removed deprecated `IdentityApi` methods: `getUserId`, `getIdToken`, and `getProfile`.
Existing usage of `getUserId` can be replaced by `getBackstageIdentity`, more precisely the equivalent of the previous `userId` can be retrieved like this:
```ts
import { parseEntityRef } from '@backstage/catalog-model';
const identity = await identityApi.getBackstageIdentity();
const { name: userId } = parseEntityRef(identity.userEntityRef);
```
Note that it is recommended to consume the entire `userEntityRef` rather than parsing out just the name, in order to support namespaces.
Existing usage of `getIdToken` can be replaced by `getCredentials`, like this:
```ts
const { token } = await identityApi.getCredentials();
```
And existing usage of `getProfile` is replaced by `getProfileInfo`, which returns the same profile object, but is now async.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-app-api': minor
'@backstage/core-plugin-api': minor
---
Removed deprecated `SignInResult` type, which was replaced with the new `onSignInSuccess` callback.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Deprecated `loadIdentityOwnerRefs`, since they can now be retrieved as `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead.
+25
View File
@@ -0,0 +1,25 @@
---
'@backstage/plugin-azure-devops': patch
'@backstage/plugin-badges': patch
'@backstage/plugin-bazaar': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-cost-insights': patch
'@backstage/plugin-fossa': patch
'@backstage/plugin-ilert': patch
'@backstage/plugin-kafka': patch
'@backstage/plugin-kubernetes': patch
'@backstage/plugin-pagerduty': patch
'@backstage/plugin-permission-react': patch
'@backstage/plugin-rollbar': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-search': patch
'@backstage/plugin-sentry': patch
'@backstage/plugin-sonarqube': patch
'@backstage/plugin-tech-insights': patch
'@backstage/plugin-techdocs': patch
'@backstage/plugin-todo': patch
---
Migrated usage of deprecated `IdentityApi` methods.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/core-plugin-api': minor
---
Removed the deprecated `id` field of `BackstageIdentityResponse`.
Existing usage can be replaced by parsing the `name` of the `identity.userEntityRef` with `parseEntityRef` from `@backstage/catalog-model`, although note that it is recommended to consume the entire `userEntityRef` in order to support namespaces.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': minor
---
Removed deprecated `BackstageIdentity` type, which was replaced by `BackstageIdentityResponse`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': minor
---
Removed deprecated `OAuthRequestApi` types: `AuthProvider`, `AuthRequesterOptions`, `AuthRequester`, and `PendingAuthRequest`.
@@ -218,7 +218,7 @@ export class MyApi implements MyInterface {
async getMyData() {
const backendUrl = this.configApi.getString('backend.baseUrl');
+ const token = await this.identityApi.getIdToken();
+ const { token } = await this.identityApi.getCredentials();
const requestUrl = `${backendUrl}/api/data/`;
- const response = await fetch(requestUrl);
+ const response = await fetch(
+8 -16
View File
@@ -19,8 +19,8 @@ import { atlassianAuthApiRef } from '@backstage/core-plugin-api';
import { auth0AuthApiRef } from '@backstage/core-plugin-api';
import { AuthProviderInfo } from '@backstage/core-plugin-api';
import { AuthRequestOptions } from '@backstage/core-plugin-api';
import { BackstageIdentity } from '@backstage/core-plugin-api';
import { BackstageIdentityApi } from '@backstage/core-plugin-api';
import { BackstageIdentityResponse } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { bitbucketAuthApiRef } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
@@ -280,7 +280,7 @@ export type BitbucketSession = {
expiresAt?: Date;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
// @public
@@ -386,7 +386,7 @@ export class GithubAuth implements OAuthApi, SessionApi {
// (undocumented)
getBackstageIdentity(
options?: AuthRequestOptions,
): Promise<BackstageIdentity | undefined>;
): Promise<BackstageIdentityResponse | undefined>;
// (undocumented)
getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>;
// (undocumented)
@@ -407,7 +407,7 @@ export type GithubSession = {
expiresAt?: Date;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
// @public
@@ -465,7 +465,7 @@ export class OAuth2
// (undocumented)
getBackstageIdentity(
options?: AuthRequestOptions,
): Promise<BackstageIdentity | undefined>;
): Promise<BackstageIdentityResponse | undefined>;
// (undocumented)
getIdToken(options?: AuthRequestOptions): Promise<string>;
// (undocumented)
@@ -492,7 +492,7 @@ export type OAuth2Session = {
expiresAt: Date;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
// @public
@@ -540,7 +540,7 @@ export class SamlAuth
// (undocumented)
getBackstageIdentity(
options?: AuthRequestOptions,
): Promise<BackstageIdentity | undefined>;
): Promise<BackstageIdentityResponse | undefined>;
// (undocumented)
getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>;
// (undocumented)
@@ -555,7 +555,7 @@ export class SamlAuth
export type SamlSession = {
userId: string;
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
// @public
@@ -563,14 +563,6 @@ export type SignInPageProps = {
onSignInSuccess(identityApi: IdentityApi): void;
};
// @public @deprecated
export type SignInResult = {
userId: string;
profile: ProfileInfo;
getIdToken?: () => Promise<string>;
signOut?: () => Promise<void>;
};
// @public
export class UnhandledErrorForwarder {
static forward(errorApi: ErrorApi, errorContext: ErrorApiErrorContext): void;
@@ -0,0 +1,82 @@
/*
* Copyright 2020 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 { withLogCollector } from '@backstage//test-utils';
import { AppIdentityProxy } from './AppIdentityProxy';
describe('AppIdentityProxy', () => {
const mockIdentityApi = {
getBackstageIdentity: jest.fn(),
getProfileInfo: jest.fn(),
getCredentials: jest.fn(),
signOut: jest.fn(),
};
beforeEach(() => {
jest.resetAllMocks();
});
it('should forward user identities', async () => {
const proxy = new AppIdentityProxy();
proxy.setTarget(mockIdentityApi);
const logs = await withLogCollector(async () => {
mockIdentityApi.getBackstageIdentity.mockResolvedValueOnce({
type: 'user',
userEntityRef: 'user:default/foo',
ownershipEntityRefs: [],
});
await expect(proxy.getBackstageIdentity()).resolves.toEqual({
type: 'user',
userEntityRef: 'user:default/foo',
ownershipEntityRefs: [],
});
});
expect(logs).toEqual({
log: [],
warn: [],
error: [],
});
});
it('should warn about invalid user entity refs', async () => {
const proxy = new AppIdentityProxy();
proxy.setTarget(mockIdentityApi);
const logs = await withLogCollector(async () => {
mockIdentityApi.getBackstageIdentity.mockResolvedValueOnce({
type: 'user',
userEntityRef: 'bar',
ownershipEntityRefs: [],
});
await expect(proxy.getBackstageIdentity()).resolves.toEqual({
type: 'user',
userEntityRef: 'bar',
ownershipEntityRefs: [],
});
});
expect(logs).toEqual({
log: [],
warn: [
`WARNING: The App IdentityApi provided an invalid userEntityRef, 'bar'. ` +
`It must be a full Entity Reference of the form '<kind>:<namespace>/<name>'.`,
],
error: [],
});
});
});
@@ -26,15 +26,30 @@ function mkError(thing: string) {
);
}
function logDeprecation(thing: string) {
// eslint-disable-next-line no-console
console.warn(
`WARNING: Call to ${thing} is deprecated and will break in the future`,
);
}
// We use this for a period of backwards compatibility. It is a hidden
// compatibility that will allow old plugins to continue working for a limited time.
type CompatibilityIdentityApi = IdentityApi & {
getUserId?(): string;
getIdToken?(): Promise<string | undefined>;
getProfile?(): ProfileInfo;
};
/**
* Implementation of the connection between the App-wide IdentityApi
* and sign-in page.
*/
export class AppIdentityProxy implements IdentityApi {
private target?: IdentityApi;
private target?: CompatibilityIdentityApi;
// This is called by the app manager once the sign-in page provides us with an implementation
setTarget(identityApi: IdentityApi) {
setTarget(identityApi: CompatibilityIdentityApi) {
this.target = identityApi;
}
@@ -42,6 +57,10 @@ export class AppIdentityProxy implements IdentityApi {
if (!this.target) {
throw mkError('getUserId');
}
if (!this.target.getUserId) {
throw new Error('IdentityApi does not implement getUserId');
}
logDeprecation('getUserId');
return this.target.getUserId();
}
@@ -49,6 +68,10 @@ export class AppIdentityProxy implements IdentityApi {
if (!this.target) {
throw mkError('getProfile');
}
if (!this.target.getProfile) {
throw new Error('IdentityApi does not implement getProfile');
}
logDeprecation('getProfile');
return this.target.getProfile();
}
@@ -63,7 +86,16 @@ export class AppIdentityProxy implements IdentityApi {
if (!this.target) {
throw mkError('getBackstageIdentity');
}
return this.target.getBackstageIdentity();
const identity = await this.target.getBackstageIdentity();
if (!identity.userEntityRef.match(/^.*:.*\/.*$/)) {
// eslint-disable-next-line no-console
console.warn(
`WARNING: The App IdentityApi provided an invalid userEntityRef, '${identity.userEntityRef}'. ` +
`It must be a full Entity Reference of the form '<kind>:<namespace>/<name>'.`,
);
}
return identity;
}
async getCredentials(): Promise<{ token?: string | undefined }> {
@@ -77,6 +109,10 @@ export class AppIdentityProxy implements IdentityApi {
if (!this.target) {
throw mkError('getIdToken');
}
if (!this.target.getIdToken) {
throw new Error('IdentityApi does not implement getIdToken');
}
logDeprecation('getIdToken');
return this.target.getIdToken();
}
@@ -15,7 +15,7 @@
*/
import {
BackstageIdentity,
BackstageIdentityResponse,
bitbucketAuthApiRef,
ProfileInfo,
} from '@backstage/core-plugin-api';
@@ -30,7 +30,7 @@ export type BitbucketAuthResponse = {
expiresInSeconds: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
const DEFAULT_PROVIDER = {
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api';
import {
ProfileInfo,
BackstageIdentityResponse,
} from '@backstage/core-plugin-api';
/**
* Session information for Bitbucket auth.
@@ -28,5 +31,5 @@ export type BitbucketSession = {
expiresAt?: Date;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
@@ -16,7 +16,7 @@
import {
AuthRequestOptions,
BackstageIdentity,
BackstageIdentityResponse,
OAuthApi,
ProfileInfo,
SessionApi,
@@ -41,7 +41,7 @@ export type GithubAuthResponse = {
expiresInSeconds?: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
const DEFAULT_PROVIDER = {
@@ -145,7 +145,7 @@ export default class GithubAuth implements OAuthApi, SessionApi {
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
): Promise<BackstageIdentityResponse | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api';
import {
ProfileInfo,
BackstageIdentityResponse,
} from '@backstage/core-plugin-api';
import { z } from 'zod';
// TODO(Rugvip): Make GithubSession internal
@@ -33,7 +36,7 @@ export type GithubSession = {
};
profile: ProfileInfo;
// TODO(Rugvip): This should be made optional once the type is no longer public
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
export const githubSessionSchema: z.ZodSchema<GithubSession> = z.object({
@@ -19,7 +19,7 @@ import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import {
AuthRequestOptions,
BackstageIdentity,
BackstageIdentityResponse,
OAuthApi,
OpenIdConnectApi,
ProfileInfo,
@@ -48,7 +48,7 @@ export type OAuth2Response = {
expiresInSeconds: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
const DEFAULT_PROVIDER = {
@@ -159,7 +159,7 @@ export default class OAuth2
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
): Promise<BackstageIdentityResponse | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api';
import {
ProfileInfo,
BackstageIdentityResponse,
} from '@backstage/core-plugin-api';
export type { OAuth2CreateOptions } from './OAuth2';
/**
@@ -30,5 +33,5 @@ export type OAuth2Session = {
expiresAt: Date;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
@@ -16,12 +16,12 @@
import {
AuthRequestOptions,
BackstageIdentity,
BackstageIdentityApi,
ProfileInfo,
ProfileInfoApi,
SessionApi,
SessionState,
BackstageIdentityResponse,
} from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { DirectAuthConnector } from '../../../../lib/AuthConnector';
@@ -35,7 +35,7 @@ import { SamlSession, samlSessionSchema } from './types';
export type SamlAuthResponse = {
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
const DEFAULT_PROVIDER = {
@@ -95,7 +95,7 @@ export default class SamlAuth
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
): Promise<BackstageIdentityResponse | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { BackstageIdentity, ProfileInfo } from '@backstage/core-plugin-api';
import {
BackstageIdentityResponse,
ProfileInfo,
} from '@backstage/core-plugin-api';
import { z } from 'zod';
/**
@@ -26,13 +29,13 @@ import { z } from 'zod';
export type ExportedSamlSession = {
userId: string;
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
/** @internal */
export type SamlSession = {
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity: BackstageIdentityResponse;
};
/** @internal */
-26
View File
@@ -18,7 +18,6 @@ import { ComponentType } from 'react';
import {
AnyApiFactory,
AppTheme,
ProfileInfo,
IconComponent,
BackstagePlugin,
RouteRef,
@@ -38,31 +37,6 @@ export type BootErrorPageProps = {
error: Error;
};
/**
* The outcome of signing in on the sign-in page.
*
* @public
* @deprecated replaced by passing the {@link @backstage/core-plugin-api#IdentityApi} to the {@link SignInPageProps.onSignInSuccess} instead.
*/
export type SignInResult = {
/**
* User ID that will be returned by the IdentityApi
*/
userId: string;
profile: ProfileInfo;
/**
* Function used to retrieve an ID token for the signed in user.
*/
getIdToken?: () => Promise<string>;
/**
* Sign out handler that will be called if the user requests to sign out.
*/
signOut?: () => Promise<void>;
};
/**
* Props for the `SignInPage` component of {@link AppComponents}.
*
+6 -2
View File
@@ -39,7 +39,6 @@ import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { SessionApi } from '@backstage/core-plugin-api';
import { SignInPageProps } from '@backstage/core-plugin-api';
import { SignInResult } from '@backstage/core-plugin-api';
import { SparklinesLineProps } from 'react-sparklines';
import { SparklinesProps } from 'react-sparklines';
import { StyledComponentProps } from '@material-ui/core/styles';
@@ -2424,7 +2423,12 @@ export class UserIdentity implements IdentityApi {
profile?: ProfileInfo;
}): IdentityApi;
static createGuest(): IdentityApi;
static fromLegacy(result: SignInResult): IdentityApi;
static fromLegacy(result: {
userId: string;
profile: ProfileInfo;
getIdToken?: () => Promise<string>;
signOut?: () => Promise<void>;
}): IdentityApi;
// (undocumented)
getBackstageIdentity(): Promise<BackstageUserIdentity>;
// (undocumented)
@@ -20,30 +20,46 @@ import {
ProfileInfo,
} from '@backstage/core-plugin-api';
// Similar to the AppIdentityApi we provide backwards compatibility for a limited time
type CompatibilityIdentityApi = IdentityApi & {
getUserId?(): string;
getIdToken?(): Promise<string | undefined>;
getProfile?(): ProfileInfo;
};
export class IdentityApiSignOutProxy implements IdentityApi {
private constructor(
private readonly config: {
identityApi: IdentityApi;
identityApi: CompatibilityIdentityApi;
signOut: IdentityApi['signOut'];
},
) {}
static from(config: {
identityApi: IdentityApi;
identityApi: CompatibilityIdentityApi;
signOut: IdentityApi['signOut'];
}): IdentityApi {
return new IdentityApiSignOutProxy(config);
}
getUserId(): string {
if (!this.config.identityApi.getUserId) {
throw new Error(`SignOutProxy IdentityApi.getUserId is not implemented`);
}
return this.config.identityApi.getUserId();
}
getIdToken(): Promise<string | undefined> {
if (!this.config.identityApi.getIdToken) {
throw new Error(`SignOutProxy IdentityApi.getIdToken is not implemented`);
}
return this.config.identityApi.getIdToken();
}
getProfile(): ProfileInfo {
if (!this.config.identityApi.getProfile) {
throw new Error(`SignOutProxy IdentityApi.getProfile is not implemented`);
}
return this.config.identityApi.getProfile();
}
@@ -18,7 +18,6 @@ import {
IdentityApi,
ProfileInfo,
BackstageUserIdentity,
SignInResult,
} from '@backstage/core-plugin-api';
function parseJwtPayload(token: string) {
@@ -26,14 +25,22 @@ function parseJwtPayload(token: string) {
return JSON.parse(atob(payload));
}
type LegacySignInResult = {
userId: string;
profile: ProfileInfo;
getIdToken?: () => Promise<string>;
signOut?: () => Promise<void>;
};
/** @internal */
export class LegacyUserIdentity implements IdentityApi {
private constructor(private readonly result: SignInResult) {}
private constructor(private readonly result: LegacySignInResult) {}
getUserId(): string {
return this.result.userId;
}
static fromResult(result: SignInResult): LegacyUserIdentity {
static fromResult(result: LegacySignInResult): LegacyUserIdentity {
return new LegacyUserIdentity(result);
}
@@ -21,12 +21,17 @@ import {
BackstageUserIdentity,
BackstageIdentityApi,
SessionApi,
SignInResult,
} from '@backstage/core-plugin-api';
import { GuestUserIdentity } from './GuestUserIdentity';
import { LegacyUserIdentity } from './LegacyUserIdentity';
// TODO(Rugvip): This and the other IdentityApi implementations still implement
// the old removed methods. This is to allow for backwards compatibility
// with old plugins that still consume this API. We will leave these in
// place as a hidden compatibility for a couple of months.
// The AppIdentityProxy warns in case any of these methods are called.
/**
* An implementation of the IdentityApi that is constructed using
* various backstage user identity representations.
@@ -49,7 +54,24 @@ export class UserIdentity implements IdentityApi {
*
* @public
*/
static fromLegacy(result: SignInResult): IdentityApi {
static fromLegacy(result: {
/**
* User ID that will be returned by the IdentityApi
*/
userId: string;
profile: ProfileInfo;
/**
* Function used to retrieve an ID token for the signed in user.
*/
getIdToken?: () => Promise<string>;
/**
* Sign out handler that will be called if the user requests to sign out.
*/
signOut?: () => Promise<void>;
}): IdentityApi {
return LegacyUserIdentity.fromResult(result);
}
-28
View File
@@ -12,7 +12,6 @@ import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api';
import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api';
import { JsonValue } from '@backstage/types';
import { Observable } from '@backstage/types';
import { ProfileInfo as ProfileInfo_2 } from '@backstage/core-plugin-api';
import { default as React_2 } from 'react';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
@@ -195,9 +194,6 @@ export const auth0AuthApiRef: ApiRef<
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
>;
// @public @deprecated (undocumented)
export type AuthProvider = Omit<AuthProviderInfo, 'id'>;
// @public
export type AuthProviderInfo = {
id: string;
@@ -205,21 +201,12 @@ export type AuthProviderInfo = {
icon: IconComponent;
};
// @public @deprecated (undocumented)
export type AuthRequester<T> = OAuthRequester<T>;
// @public @deprecated (undocumented)
export type AuthRequesterOptions<T> = OAuthRequesterOptions<T>;
// @public
export type AuthRequestOptions = {
optional?: boolean;
instantPopup?: boolean;
};
// @public @deprecated
export type BackstageIdentity = BackstageIdentityResponse;
// @public
export type BackstageIdentityApi = {
getBackstageIdentity(
@@ -229,7 +216,6 @@ export type BackstageIdentityApi = {
// @public
export type BackstageIdentityResponse = {
id: string;
token: string;
identity: BackstageUserIdentity;
};
@@ -512,9 +498,6 @@ export type IconComponent = ComponentType<{
// @public
export type IdentityApi = {
getUserId(): string;
getIdToken(): Promise<string | undefined>;
getProfile(): ProfileInfo;
getProfileInfo(): Promise<ProfileInfo>;
getBackstageIdentity(): Promise<BackstageUserIdentity>;
getCredentials(): Promise<{
@@ -657,9 +640,6 @@ export type PathParams<S extends string> = {
[name in ParamNames<S>]: string;
};
// @public @deprecated (undocumented)
export type PendingAuthRequest = PendingOAuthRequest;
// @public
export type PendingOAuthRequest = {
provider: Omit<AuthProviderInfo, 'id'> & {
@@ -732,14 +712,6 @@ export type SignInPageProps = {
onSignInSuccess(identityApi: IdentityApi_2): void;
};
// @public @deprecated
export type SignInResult = {
userId: string;
profile: ProfileInfo_2;
getIdToken?: () => Promise<string>;
signOut?: () => Promise<void>;
};
// @public
export interface StorageApi {
forBucket(name: string): StorageApi;
@@ -22,31 +22,6 @@ import { BackstageUserIdentity, ProfileInfo } from './auth';
* @public
*/
export type IdentityApi = {
/**
* The ID of the signed in user. This ID is not meant to be presented to the user, but used
* as an opaque string to pass on to backends or use in frontend logic.
*
* @deprecated use {@link IdentityApi.getBackstageIdentity} instead.
*/
getUserId(): string;
/**
* An OpenID Connect ID Token which proves the identity of the signed in user.
*
* The ID token will be undefined if the signed in user does not have a verified
* identity, such as a demo user or mocked user for e2e tests.
*
* @deprecated use {@link IdentityApi.getCredentials} instead.
*/
getIdToken(): Promise<string | undefined>;
/**
* The profile of the signed in user.
*
* @deprecated use {@link IdentityApi.getProfileInfo} instead.
*/
getProfile(): ProfileInfo;
/**
* The profile of the signed in user.
*/
@@ -18,12 +18,6 @@ import { Observable } from '@backstage/types';
import { ApiRef, createApiRef } from '../system';
import { AuthProviderInfo } from './auth';
/**
* @public
* @deprecated Use AuthProviderInfo instead
*/
export type AuthProvider = Omit<AuthProviderInfo, 'id'>;
/**
* Describes how to handle auth requests. Both how to show them to the user, and what to do when
* the user accesses the auth request.
@@ -45,12 +39,6 @@ export type OAuthRequesterOptions<TOAuthResponse> = {
onAuthRequest(scopes: Set<string>): Promise<TOAuthResponse>;
};
/**
* @public
* @deprecated Use OAuthRequesterOptions instead
*/
export type AuthRequesterOptions<T> = OAuthRequesterOptions<T>;
/**
* Function used to trigger new auth requests for a set of scopes.
*
@@ -69,12 +57,6 @@ export type OAuthRequester<TAuthResponse> = (
scopes: Set<string>,
) => Promise<TAuthResponse>;
/**
* @public
* @deprecated Use OAuthRequester instead
*/
export type AuthRequester<T> = OAuthRequester<T>;
/**
* An pending auth request for a single auth provider. The request will remain in this pending
* state until either reject() or trigger() is called.
@@ -107,12 +89,6 @@ export type PendingOAuthRequest = {
trigger(): Promise<void>;
};
/**
* @public
* @deprecated Use PendingOAuthRequest instead
*/
export type PendingAuthRequest = PendingOAuthRequest;
/**
* Provides helpers for implemented OAuth login flows within Backstage.
*
@@ -221,13 +221,6 @@ export type BackstageUserIdentity = {
* @public
*/
export type BackstageIdentityResponse = {
/**
* The backstage user ID.
*
* @deprecated The identity is now provided via the `identity` field instead.
*/
id: string;
/**
* The token used to authenticate the user within Backstage.
*/
@@ -239,14 +232,6 @@ export type BackstageIdentityResponse = {
identity: BackstageUserIdentity;
};
/**
* The old exported symbol for {@link BackstageIdentityResponse}.
*
* @public
* @deprecated use {@link BackstageIdentityResponse} instead.
*/
export type BackstageIdentity = BackstageIdentityResponse;
/**
* Profile information of the user.
*
@@ -20,7 +20,6 @@
// eslint-disable-next-line no-restricted-imports
export type {
BootErrorPageProps,
SignInResult,
SignInPageProps,
ErrorBoundaryFallbackProps,
AppComponents,
@@ -96,7 +96,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
const baseUrl = `${await this.discoveryApi.getBaseUrl('azure-devops')}/`;
const url = new URL(path, baseUrl);
const idToken = await this.identityApi.getIdToken();
const { token: idToken } = await this.identityApi.getCredentials();
const response = await fetch(url.toString(), {
headers: idToken ? { Authorization: `Bearer ${idToken}` } : {},
});
@@ -15,8 +15,10 @@
*/
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
export function useUserEmail(): string | undefined {
const identityApi = useApi(identityApiRef);
return identityApi.getProfile().email;
const state = useAsync(() => identityApi.getProfileInfo(), [identityApi]);
return state.value?.email;
}
+1 -1
View File
@@ -34,7 +34,7 @@ export class BadgesClient implements BadgesApi {
public async getEntityBadgeSpecs(entity: Entity): Promise<BadgeSpec[]> {
const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity);
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const response = await fetch(entityBadgeSpecsUrl, {
headers: token
? {
+2 -3
View File
@@ -121,6 +121,7 @@ export class BazaarClient implements BazaarApi {
async addMember(id: number, userId: string): Promise<void> {
const baseUrl = await this.discoveryApi.getBaseUrl('bazaar');
const { picture } = await this.identityApi.getProfileInfo();
await fetch(
`${baseUrl}/projects/${encodeURIComponent(
@@ -132,9 +133,7 @@ export class BazaarClient implements BazaarApi {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
picture: (await this.identityApi.getProfileInfo()).picture,
}),
body: JSON.stringify({ picture }),
},
);
}
@@ -67,21 +67,12 @@ describe('CatalogImportClient', () => {
getCredentials: jest.fn().mockResolvedValue({ token: 'token' }),
};
const identityApi = {
getUserId: () => {
return 'user';
},
getProfile: () => {
return {};
},
getIdToken: () => {
return Promise.resolve('token');
},
signOut: () => {
return Promise.resolve();
},
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
getCredentials: jest.fn().mockResolvedValue({ token: 'token' }),
};
const scmIntegrationsApi = ScmIntegrations.fromConfig(
@@ -173,13 +173,13 @@ the component will become available.\n\nFor more information, read an \
}: {
repo: string;
}): Promise<PartialEntity[]> {
const idToken = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const response = await fetch(
`${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`,
{
headers: {
'Content-Type': 'application/json',
...(idToken && { Authorization: `Bearer ${idToken}` }),
...(token && { Authorization: `Bearer ${token}` }),
},
method: 'POST',
body: JSON.stringify({
+1 -1
View File
@@ -781,7 +781,7 @@ export function loadCatalogOwnerRefs(
identityOwnerRefs: string[],
): Promise<string[]>;
// @public
// @public @deprecated
export function loadIdentityOwnerRefs(
identityApi: IdentityApi,
): Promise<string[]>;
@@ -68,8 +68,12 @@ const mockConfigApi = {
getOptionalString: () => '',
} as Partial<ConfigApi>;
const mockIdentityApi: Partial<IdentityApi> = {
getUserId: () => 'guest',
getIdToken: async () => undefined,
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: [],
}),
getCredentials: async () => ({ token: undefined }),
};
const mockCatalogApi: Partial<CatalogApi> = {
getEntities: jest.fn().mockImplementation(async () => ({ items: entities })),
@@ -33,14 +33,11 @@ import {
} from './useEntityOwnership';
describe('useEntityOwnership', () => {
type MockIdentityApi = jest.Mocked<
Pick<IdentityApi, 'getUserId' | 'getIdToken'>
>;
type MockIdentityApi = jest.Mocked<Pick<IdentityApi, 'getBackstageIdentity'>>;
type MockCatalogApi = jest.Mocked<Pick<CatalogApi, 'getEntityByName'>>;
const mockIdentityApi: MockIdentityApi = {
getUserId: jest.fn(),
getIdToken: jest.fn(),
getBackstageIdentity: jest.fn(),
};
const mockCatalogApi: MockCatalogApi = {
getEntityByName: jest.fn(),
@@ -100,80 +97,19 @@ describe('useEntityOwnership', () => {
],
};
// these were generated on https://jwt.io, based off of its default example token
// no ent at all
const tokenNoEnt =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
// "ent": []
const tokenEmptyEnt =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbnQiOltdfQ.Khyza2whczkoC4wSCLBhBaBB9-ktIkk7gpXEgQPHhtY';
// "ent": ["user:default/user1"]
const tokenUserEnt =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbnQiOlsidXNlcjpkZWZhdWx0L3VzZXIxIl19.CMCxjwI4rj_TD3uUoBNgFjkZI23LwRTbQnSPBxzncoY';
// "ent": ["user:default/user1", "group:default/group1"]
const tokenUserAndGroupEnt =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbnQiOlsidXNlcjpkZWZhdWx0L3VzZXIxIiwiZ3JvdXA6ZGVmYXVsdC9ncm91cDEiXX0.ZZmZrogbQKx0hnForw63ETkyAhUyeoBE8Hgloi45rdg';
afterEach(() => {
jest.resetAllMocks();
});
describe('loadIdentityOwnerRefs', () => {
it('returns the user id when there is no relevant token info', async () => {
mockIdentityApi.getUserId.mockReturnValueOnce('foo');
mockIdentityApi.getIdToken.mockResolvedValueOnce(undefined);
await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([
'user:default/foo',
]);
mockIdentityApi.getUserId.mockReturnValueOnce('ns/foo');
mockIdentityApi.getIdToken.mockResolvedValueOnce(undefined);
await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([
'user:ns/foo',
]);
mockIdentityApi.getUserId.mockReturnValueOnce('user:ns/foo');
mockIdentityApi.getIdToken.mockResolvedValueOnce(undefined);
await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([
'user:ns/foo',
]);
mockIdentityApi.getUserId.mockReturnValueOnce('foo');
mockIdentityApi.getIdToken.mockResolvedValueOnce(tokenNoEnt);
await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([
'user:default/foo',
]);
mockIdentityApi.getUserId.mockReturnValueOnce('foo');
mockIdentityApi.getIdToken.mockResolvedValueOnce(tokenEmptyEnt);
await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([
'user:default/foo',
]);
});
it('returns both the user id and the token parts', async () => {
mockIdentityApi.getUserId.mockReturnValueOnce('foo');
mockIdentityApi.getIdToken.mockResolvedValueOnce(tokenUserEnt);
await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([
'user:default/foo',
'user:default/user1',
]);
mockIdentityApi.getUserId.mockReturnValueOnce('foo');
mockIdentityApi.getIdToken.mockResolvedValueOnce(tokenUserAndGroupEnt);
await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([
'user:default/foo',
'user:default/user1',
'group:default/group1',
]);
});
it('gracefully ignores broken token', async () => {
mockIdentityApi.getUserId.mockReturnValueOnce('foo');
mockIdentityApi.getIdToken.mockResolvedValueOnce('not a jwt');
await expect(loadIdentityOwnerRefs(identityApi)).resolves.toEqual([
'user:default/foo',
]);
it('passes through the ownershipEntityRefs', async () => {
const refs = new Array<string>();
mockIdentityApi.getBackstageIdentity.mockResolvedValueOnce({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: refs,
});
await expect(loadIdentityOwnerRefs(identityApi)).resolves.toBe(refs);
});
});
@@ -204,9 +140,12 @@ describe('useEntityOwnership', () => {
});
describe('useEntityOwnership', () => {
it('matches ownership via token claims', async () => {
mockIdentityApi.getUserId.mockReturnValue('foo');
mockIdentityApi.getIdToken.mockResolvedValue(tokenUserAndGroupEnt);
it('matches ownership via ownership entity refs', async () => {
mockIdentityApi.getBackstageIdentity.mockResolvedValue({
type: 'user',
userEntityRef: 'user:default/user1',
ownershipEntityRefs: ['user:default/user1', 'group:default/group1'],
});
mockCatalogApi.getEntityByName.mockResolvedValue(undefined);
const { result, waitForValueToChange } = renderHook(
@@ -224,32 +163,5 @@ describe('useEntityOwnership', () => {
expect(result.current.loading).toBe(false);
expect(result.current.isOwnedEntity(ownedEntity)).toBe(true);
});
it('matches ownership via catalog user entity', async () => {
mockIdentityApi.getUserId.mockReturnValue('user2');
mockIdentityApi.getIdToken.mockResolvedValue(undefined);
mockCatalogApi.getEntityByName.mockResolvedValue(user2Entity);
const { result, waitForValueToChange } = renderHook(
() => useEntityOwnership(),
{
wrapper: Wrapper,
},
);
expect(result.current.loading).toBe(true);
expect(result.current.isOwnedEntity(ownedEntity)).toBe(false);
await waitForValueToChange(() => result.current.loading);
expect(result.current.loading).toBe(false);
expect(result.current.isOwnedEntity(ownedEntity)).toBe(true);
expect(mockCatalogApi.getEntityByName).toBeCalledWith({
kind: 'user',
namespace: 'default',
name: 'user2',
});
});
});
});
@@ -28,33 +28,18 @@ import {
identityApiRef,
useApi,
} from '@backstage/core-plugin-api';
import jwtDecoder from 'jwt-decode';
import { useMemo } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { catalogApiRef } from '../api';
import { getEntityRelations } from '../utils/getEntityRelations';
// Takes a user ID from the identity, which can be on basically any form, and
// returns an entity ref. E.g. if the input is "foo", it returns
// "user:default/foo" to make sure it's a full ref.
function extendUserId(id: string): string {
try {
const ref = parseEntityRef(id, {
defaultKind: 'User',
defaultNamespace: 'default',
});
return stringifyEntityRef(ref);
} catch {
return id;
}
}
/**
* Takes the relevant parts of the Backstage identity, and translates them into
* a list of entity refs on string form that represent the user's ownership
* connections.
*
* @public
* @deprecated Use `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead.
*
* @param identityApi - The IdentityApi implementation
* @returns IdentityOwner refs as a string array
@@ -62,30 +47,8 @@ function extendUserId(id: string): string {
export async function loadIdentityOwnerRefs(
identityApi: IdentityApi,
): Promise<string[]> {
const id = identityApi.getUserId();
const token = await identityApi.getIdToken();
const result: string[] = [];
if (id) {
result.push(extendUserId(id));
}
if (token) {
try {
const decoded = jwtDecoder(token) as any;
if (decoded?.ent) {
[decoded.ent]
.flat()
.filter(x => typeof x === 'string')
.map(x => x.toLocaleLowerCase('en-US'))
.forEach(x => result.push(x));
}
} catch {
// ignore
}
}
return result;
const identity = await identityApi.getBackstageIdentity();
return identity.ownershipEntityRefs;
}
/**
@@ -142,9 +105,12 @@ export function useEntityOwnership(): {
// Trigger load only on mount
const { loading, value: refs } = useAsync(async () => {
const identityRefs = await loadIdentityOwnerRefs(identityApi);
const catalogRefs = await loadCatalogOwnerRefs(catalogApi, identityRefs);
return new Set([...identityRefs, ...catalogRefs]);
const { ownershipEntityRefs } = await identityApi.getBackstageIdentity();
const catalogRefs = await loadCatalogOwnerRefs(
catalogApi,
ownershipEntityRefs,
);
return new Set([...ownershipEntityRefs, ...catalogRefs]);
}, []);
const isOwnedEntity = useMemo(() => {
+14 -12
View File
@@ -14,7 +14,11 @@
* limitations under the License.
*/
import { UserEntity } from '@backstage/catalog-model';
import {
ENTITY_DEFAULT_NAMESPACE,
parseEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
import { catalogApiRef } from '../api';
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
@@ -26,15 +30,13 @@ export function useOwnUser(): AsyncState<UserEntity | undefined> {
const catalogApi = useApi(catalogApiRef);
const identityApi = useApi(identityApiRef);
// TODO: get the full entity (or at least the full entity name) from the
// identityApi
return useAsync(
() =>
catalogApi.getEntityByName({
kind: 'User',
namespace: 'default',
name: identityApi.getUserId(),
}) as Promise<UserEntity | undefined>,
[catalogApi, identityApi],
);
return useAsync(async () => {
const identity = await identityApi.getBackstageIdentity();
return catalogApi.getEntityByName(
parseEntityRef(identity.userEntityRef, {
defaultKind: 'User',
defaultNamespace: ENTITY_DEFAULT_NAMESPACE,
}),
) as Promise<UserEntity | undefined>;
}, [catalogApi, identityApi]);
}
@@ -28,38 +28,16 @@ const discoveryApi: DiscoveryApi = {
},
};
const identityApi: IdentityApi = {
getUserId() {
return 'jane-fonda';
},
getProfile() {
return { email: 'jane-fonda@spotify.com' };
},
async getIdToken() {
return Promise.resolve('fake-id-token');
},
async signOut() {
return Promise.resolve();
},
signOut: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
getCredentials: jest.fn().mockResolvedValue({ token: 'fake-id-token' }),
};
const guestIdentityApi: IdentityApi = {
getUserId() {
return 'guest';
},
getProfile() {
return {};
},
async getIdToken() {
return Promise.resolve(undefined);
},
async signOut() {
return Promise.resolve();
},
signOut: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
getCredentials: jest.fn().mockResolvedValue({ token: undefined }),
};
describe('CatalogClientWrapper', () => {
+49 -30
View File
@@ -51,89 +51,108 @@ export class CatalogClientWrapper implements CatalogApi {
id: string,
options?: CatalogRequestOptions,
): Promise<Location | undefined> {
return await this.client.getLocationById(id, {
token: options?.token ?? (await this.identityApi.getIdToken()),
});
return await this.client.getLocationById(
id,
await this.getCredentials(options),
);
}
async getEntities(
request?: CatalogEntitiesRequest,
options?: CatalogRequestOptions,
): Promise<CatalogListResponse<Entity>> {
return await this.client.getEntities(request, {
token: options?.token ?? (await this.identityApi.getIdToken()),
});
return await this.client.getEntities(
request,
await this.getCredentials(options),
);
}
async getEntityByName(
compoundName: EntityName,
options?: CatalogRequestOptions,
): Promise<Entity | undefined> {
return await this.client.getEntityByName(compoundName, {
token: options?.token ?? (await this.identityApi.getIdToken()),
});
return await this.client.getEntityByName(
compoundName,
await this.getCredentials(options),
);
}
async addLocation(
request: AddLocationRequest,
options?: CatalogRequestOptions,
): Promise<AddLocationResponse> {
return await this.client.addLocation(request, {
token: options?.token ?? (await this.identityApi.getIdToken()),
});
return await this.client.addLocation(
request,
await this.getCredentials(options),
);
}
async getOriginLocationByEntity(
entity: Entity,
options?: CatalogRequestOptions,
): Promise<Location | undefined> {
return await this.client.getOriginLocationByEntity(entity, {
token: options?.token ?? (await this.identityApi.getIdToken()),
});
return await this.client.getOriginLocationByEntity(
entity,
await this.getCredentials(options),
);
}
async getLocationByEntity(
entity: Entity,
options?: CatalogRequestOptions,
): Promise<Location | undefined> {
return await this.client.getLocationByEntity(entity, {
token: options?.token ?? (await this.identityApi.getIdToken()),
});
return await this.client.getLocationByEntity(
entity,
await this.getCredentials(options),
);
}
async removeLocationById(
id: string,
options?: CatalogRequestOptions,
): Promise<void> {
return await this.client.removeLocationById(id, {
token: options?.token ?? (await this.identityApi.getIdToken()),
});
return await this.client.removeLocationById(
id,
await this.getCredentials(options),
);
}
async removeEntityByUid(
uid: string,
options?: CatalogRequestOptions,
): Promise<void> {
return await this.client.removeEntityByUid(uid, {
token: options?.token ?? (await this.identityApi.getIdToken()),
});
return await this.client.removeEntityByUid(
uid,
await this.getCredentials(options),
);
}
async refreshEntity(
entityRef: string,
options?: CatalogRequestOptions,
): Promise<void> {
return await this.client.refreshEntity(entityRef, {
token: options?.token ?? (await this.identityApi.getIdToken()),
});
return await this.client.refreshEntity(
entityRef,
await this.getCredentials(options),
);
}
async getEntityAncestors(
request: CatalogEntityAncestorsRequest,
options?: CatalogRequestOptions,
): Promise<CatalogEntityAncestorsResponse> {
return await this.client.getEntityAncestors(request, {
token: options?.token ?? (await this.identityApi.getIdToken()),
});
return await this.client.getEntityAncestors(
request,
await this.getCredentials(options),
);
}
private async getCredentials(
options?: CatalogRequestOptions,
): Promise<{ token?: string }> {
if (options?.token) {
return { token: options?.token };
}
return this.identityApi.getCredentials();
}
}
@@ -120,9 +120,13 @@ describe('DefaultCatalogPage', () => {
displayName: 'Display Name',
};
const identityApi: Partial<IdentityApi> = {
getUserId: () => 'tools',
getIdToken: async () => undefined,
getProfile: () => testProfile,
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest', 'group:default/tools'],
}),
getCredentials: async () => ({ token: undefined }),
getProfileInfo: async () => testProfile,
};
const storageApi = MockStorageApi.create();
+1
View File
@@ -31,6 +31,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.9.9",
"@backstage/config": "^0.1.12",
"@backstage/core-components": "^0.8.4",
"@backstage/core-plugin-api": "^0.5.0",
@@ -23,7 +23,7 @@ import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api';
describe('<CostInsightsHeader/>', () => {
const identityApi: Partial<IdentityApi> = {
getProfile: () => ({
getProfileInfo: async () => ({
email: 'test-email@example.com',
displayName: 'User 1',
}),
@@ -16,16 +16,15 @@
import React from 'react';
import { Typography } from '@material-ui/core';
import useAsync from 'react-use/lib/useAsync';
import { useCostInsightsStyles } from '../../utils/styles';
import { Group } from '../../types';
import {
identityApiRef,
ProfileInfo,
useApi,
} from '@backstage/core-plugin-api';
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
function name(profile: ProfileInfo | undefined): string {
return profile?.displayName || 'Mysterious Stranger';
function useDisplayName(): string {
const identityApi = useApi(identityApiRef);
const state = useAsync(() => identityApi.getProfileInfo(), [identityApi]);
return state.loading ? '' : state.value?.displayName || 'Mysterious Stranger';
}
type CostInsightsHeaderProps = {
@@ -39,7 +38,7 @@ const CostInsightsHeaderNoData = ({
owner,
groups,
}: CostInsightsHeaderProps) => {
const profile = useApi(identityApiRef).getProfile();
const displayName = useDisplayName();
const classes = useCostInsightsStyles();
const hasMultipleGroups = groups.length > 1;
@@ -52,8 +51,8 @@ const CostInsightsHeaderNoData = ({
Well this is awkward
</Typography>
<Typography className={classes.h6Subtle} align="center" gutterBottom>
<b>Hey, {name(profile)}!</b> <b>{owner}</b> doesn't seem to have any
cloud costs.
<b>Hey, {displayName}!</b> <b>{owner}</b> doesn't seem to have any cloud
costs.
</Typography>
{hasMultipleGroups && (
<Typography align="center" gutterBottom>
@@ -68,7 +67,7 @@ const CostInsightsHeaderAlerts = ({
owner,
alerts,
}: CostInsightsHeaderProps) => {
const profile = useApi(identityApiRef).getProfile();
const displayName = useDisplayName();
const classes = useCostInsightsStyles();
return (
@@ -80,7 +79,7 @@ const CostInsightsHeaderAlerts = ({
You have {alerts} thing{alerts > 1 && 's'} to look into
</Typography>
<Typography className={classes.h6Subtle} align="center" gutterBottom>
<b>Hey, {name(profile)}!</b> We've identified{' '}
<b>Hey, {displayName}!</b> We've identified{' '}
{alerts > 1 ? 'a few things ' : 'one thing '}
<b>{owner}</b> should look into next.
</Typography>
@@ -89,7 +88,7 @@ const CostInsightsHeaderAlerts = ({
};
const CostInsightsHeaderNoAlerts = ({ owner }: CostInsightsHeaderProps) => {
const profile = useApi(identityApiRef).getProfile();
const displayName = useDisplayName();
const classes = useCostInsightsStyles();
return (
@@ -101,7 +100,7 @@ const CostInsightsHeaderNoAlerts = ({ owner }: CostInsightsHeaderProps) => {
Your team is doing great
</Typography>
<Typography className={classes.h6Subtle} align="center" gutterBottom>
<b>Hey, {name(profile)}!</b> <b>{owner}</b> is doing well. No major
<b>Hey, {displayName}!</b> <b>{owner}</b> is doing well. No major
changes this month.
</Typography>
</>
@@ -109,7 +108,7 @@ const CostInsightsHeaderNoAlerts = ({ owner }: CostInsightsHeaderProps) => {
};
export const CostInsightsHeaderNoGroups = () => {
const profile = useApi(identityApiRef).getProfile();
const displayName = useDisplayName();
const classes = useCostInsightsStyles();
return (
<>
@@ -120,8 +119,7 @@ export const CostInsightsHeaderNoGroups = () => {
Well this is awkward
</Typography>
<Typography className={classes.h6Subtle} align="center" gutterBottom>
<b>Hey, {name(profile)}!</b> It doesn't look like you belong to any
teams.
<b>Hey, {displayName}!</b> It doesn't look like you belong to any teams.
</Typography>
</>
);
+11 -2
View File
@@ -26,6 +26,10 @@ import { MapLoadingToProps, useLoading } from './useLoading';
import { Group, Maybe } from '../types';
import { DefaultLoadingAction } from '../utils/loading';
import { useApi, identityApiRef } from '@backstage/core-plugin-api';
import {
ENTITY_DEFAULT_NAMESPACE,
parseEntityRef,
} from '@backstage/catalog-model';
type GroupsProviderLoadingProps = {
dispatchLoadingGroups: (isLoading: boolean) => void;
@@ -47,7 +51,7 @@ export const GroupsContext = React.createContext<
>(undefined);
export const GroupsProvider = ({ children }: PropsWithChildren<{}>) => {
const userId = useApi(identityApiRef).getUserId();
const identityApi = useApi(identityApiRef);
const client = useApi(costInsightsApiRef);
const [error, setError] = useState<Maybe<Error>>(null);
const { dispatchLoadingGroups } = useLoading(mapLoadingToProps);
@@ -59,6 +63,11 @@ export const GroupsProvider = ({ children }: PropsWithChildren<{}>) => {
async function getUserGroups() {
try {
const { userEntityRef } = await identityApi.getBackstageIdentity();
const { name: userId } = parseEntityRef(userEntityRef, {
defaultKind: 'User',
defaultNamespace: ENTITY_DEFAULT_NAMESPACE,
});
const g = await client.getUserGroups(userId);
setGroups(g);
} catch (e) {
@@ -69,7 +78,7 @@ export const GroupsProvider = ({ children }: PropsWithChildren<{}>) => {
}
getUserGroups();
}, [userId, client]); // eslint-disable-line react-hooks/exhaustive-deps
}, [client]); // eslint-disable-line react-hooks/exhaustive-deps
if (error) {
return <Alert severity="error">{error.message}</Alert>;
+2 -2
View File
@@ -25,8 +25,8 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api';
const server = setupServer();
const identityApi = {
async getIdToken() {
return Promise.resolve('fake-id-token');
async getCredentials() {
return { token: 'fake-id-token' };
},
} as IdentityApi;
+1 -1
View File
@@ -57,7 +57,7 @@ export class FossaClient implements FossaApi {
query: Record<string, any>,
): Promise<T> {
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`;
const idToken = await this.identityApi.getIdToken();
const { token: idToken } = await this.identityApi.getCredentials();
const response = await fetch(
`${apiUrl}/${path}?${new URLSearchParams(query).toString()}`,
{
@@ -27,6 +27,10 @@ import {
identityApiRef,
} from '@backstage/core-plugin-api';
import { Progress, Link } from '@backstage/core-components';
import {
ENTITY_DEFAULT_NAMESPACE,
parseEntityRef,
} from '@backstage/catalog-model';
export const IncidentActionsMenu = ({
incident,
@@ -40,7 +44,6 @@ export const IncidentActionsMenu = ({
const ilertApi = useApi(ilertApiRef);
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
const userName = identityApi.getUserId();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const callback = onIncidentChanged || ((_: Incident): void => {});
const setProcessing = setIsLoading || ((_: boolean): void => {});
@@ -64,6 +67,12 @@ export const IncidentActionsMenu = ({
try {
handleCloseMenu();
setProcessing(true);
const { userEntityRef } = await identityApi.getBackstageIdentity();
const { name: userName } = parseEntityRef(userEntityRef, {
defaultKind: 'User',
defaultNamespace: ENTITY_DEFAULT_NAMESPACE,
});
const newIncident = await ilertApi.acceptIncident(incident, userName);
alertApi.post({ message: 'Incident accepted.' });
@@ -79,6 +88,11 @@ export const IncidentActionsMenu = ({
try {
handleCloseMenu();
setProcessing(true);
const { userEntityRef } = await identityApi.getBackstageIdentity();
const { name: userName } = parseEntityRef(userEntityRef, {
defaultKind: 'User',
defaultNamespace: ENTITY_DEFAULT_NAMESPACE,
});
const newIncident = await ilertApi.resolveIncident(incident, userName);
alertApi.post({ message: 'Incident resolved.' });
@@ -14,6 +14,10 @@
* limitations under the License.
*/
import React from 'react';
import {
ENTITY_DEFAULT_NAMESPACE,
parseEntityRef,
} from '@backstage/catalog-model';
import { makeStyles } from '@material-ui/core/styles';
import Alert from '@material-ui/lab/Alert';
import Button from '@material-ui/core/Button';
@@ -80,7 +84,6 @@ export const IncidentNewModal = ({
const ilertApi = useApi(ilertApiRef);
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
const userName = identityApi.getUserId();
const source = window.location.toString();
const classes = useStyles();
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
@@ -102,6 +105,11 @@ export const IncidentNewModal = ({
setIsLoading(true);
setTimeout(async () => {
try {
const { userEntityRef } = await identityApi.getBackstageIdentity();
const { name: userName } = parseEntityRef(userEntityRef, {
defaultKind: 'User',
defaultNamespace: ENTITY_DEFAULT_NAMESPACE,
});
await ilertApi.createIncident({
integrationKey,
summary,
+1 -1
View File
@@ -31,7 +31,7 @@ export class KafkaBackendClient implements KafkaApi {
private async internalGet(path: string): Promise<any> {
const url = `${await this.discoveryApi.getBaseUrl('kafka')}${path}`;
const idToken = await this.identityApi.getIdToken();
const { token: idToken } = await this.identityApi.getCredentials();
const response = await fetch(url, {
method: 'GET',
headers: {
@@ -56,7 +56,7 @@ export class KubernetesBackendClient implements KubernetesApi {
requestBody: KubernetesRequestBody,
): Promise<any> {
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
const idToken = await this.identityApi.getIdToken();
const { token: idToken } = await this.identityApi.getCredentials();
const response = await fetch(url, {
method: 'POST',
headers: {
@@ -79,7 +79,7 @@ export class KubernetesBackendClient implements KubernetesApi {
}
async getClusters(): Promise<{ name: string; authProvider: string }[]> {
const idToken = await this.identityApi.getIdToken();
const { token: idToken } = await this.identityApi.getCredentials();
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/clusters`;
const response = await fetch(url, {
@@ -22,17 +22,9 @@ import { EntityProvider } from '@backstage/plugin-catalog-react';
import { TriggerButton } from './';
import { ApiProvider } from '@backstage/core-app-api';
import {
alertApiRef,
IdentityApi,
identityApiRef,
} from '@backstage/core-plugin-api';
import { alertApiRef } from '@backstage/core-plugin-api';
describe('TriggerButton', () => {
const mockIdentityApi: Partial<IdentityApi> = {
getUserId: () => 'guest@example.com',
};
const mockTriggerAlarmFn = jest.fn();
const mockPagerDutyApi = {
triggerAlarm: mockTriggerAlarmFn,
@@ -40,7 +32,6 @@ describe('TriggerButton', () => {
const apis = TestApiRegistry.from(
[alertApiRef, {}],
[identityApiRef, mockIdentityApi],
[pagerDutyApiRef, mockPagerDutyApi],
);
@@ -30,7 +30,11 @@ import {
describe('TriggerDialog', () => {
const mockIdentityApi: Partial<IdentityApi> = {
getUserId: () => 'guest@example.com',
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: [],
}),
};
const mockTriggerAlarmFn = jest.fn();
@@ -89,7 +93,7 @@ describe('TriggerDialog', () => {
entity!.metadata!.annotations!['pagerduty.com/integration-key'],
source: window.location.toString(),
description,
userName: 'guest@example.com',
userName: 'guest',
});
});
});
@@ -34,6 +34,10 @@ import {
alertApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import {
ENTITY_DEFAULT_NAMESPACE,
parseEntityRef,
} from '@backstage/catalog-model';
type Props = {
showDialog: boolean;
@@ -49,18 +53,23 @@ export const TriggerDialog = ({
const { name, integrationKey } = usePagerdutyEntity();
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
const userName = identityApi.getUserId();
const api = useApi(pagerDutyApiRef);
const [description, setDescription] = useState<string>('');
const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn(
async (descriptions: string) =>
async (descriptions: string) => {
const { userEntityRef } = await identityApi.getBackstageIdentity();
const { name: userName } = parseEntityRef(userEntityRef, {
defaultKind: 'User',
defaultNamespace: ENTITY_DEFAULT_NAMESPACE,
});
await api.triggerAlarm({
integrationKey: integrationKey as string,
source: window.location.toString(),
description: descriptions,
userName,
}),
});
},
);
const descriptionChanged = (
@@ -73,7 +82,7 @@ export const TriggerDialog = ({
if (value) {
(async () => {
alertApi.post({
message: `Alarm successfully triggered by ${userName}`,
message: `Alarm successfully triggered`,
});
handleDialog();
@@ -83,7 +92,7 @@ export const TriggerDialog = ({
onIncidentCreated?.();
})();
}
}, [value, alertApi, handleDialog, userName, onIncidentCreated]);
}, [value, alertApi, handleDialog, onIncidentCreated]);
if (error) {
alertApi.post({
@@ -45,9 +45,10 @@ export class IdentityPermissionApi implements PermissionApi {
}
async authorize(request: AuthorizeQuery): Promise<AuthorizeDecision> {
const response = await this.permissionClient.authorize([request], {
token: await this.identityApi.getIdToken(),
});
const response = await this.permissionClient.authorize(
[request],
await this.identityApi.getCredentials(),
);
return response[0];
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ export class RollbarClient implements RollbarApi {
private async get(path: string): Promise<any> {
const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`;
const idToken = await this.identityApi.getIdToken();
const { token: idToken } = await this.identityApi.getCredentials();
const response = await fetch(url, {
headers: idToken ? { Authorization: `Bearer ${idToken}` } : {},
});
+4 -4
View File
@@ -124,7 +124,7 @@ export class ScaffolderClient implements ScaffolderApi {
): Promise<TemplateParameterSchema> {
const { namespace, kind, name } = templateName;
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const templatePath = [namespace, kind, name]
.map(s => encodeURIComponent(s))
@@ -156,7 +156,7 @@ export class ScaffolderClient implements ScaffolderApi {
templateName: string,
values: Record<string, any>,
): Promise<string> {
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v2/tasks`;
const response = await fetch(url, {
method: 'POST',
@@ -178,7 +178,7 @@ export class ScaffolderClient implements ScaffolderApi {
}
async getTask(taskId: string) {
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`;
const response = await fetch(url, {
@@ -295,7 +295,7 @@ export class ScaffolderClient implements ScaffolderApi {
*/
async listActions(): Promise<ListActionsResponse> {
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const response = await fetch(`${baseUrl}/v2/actions`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
+4 -7
View File
@@ -27,16 +27,13 @@ describe('apis', () => {
const getBaseUrl = jest.fn().mockResolvedValue(baseUrl);
const token = 'AUTHTOKEN';
const withToken = jest.fn().mockResolvedValue(token);
const withoutToken = jest.fn().mockResolvedValue(undefined);
const createIdentityApiMock = (getIdToken: any) => ({
getIdToken,
getUserId: jest.fn(),
getProfile: jest.fn(),
const withToken = jest.fn().mockResolvedValue({ token });
const withoutToken = jest.fn().mockResolvedValue({ token: undefined });
const createIdentityApiMock = (getCredentials: any) => ({
signOut: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
getCredentials,
});
const client = new SearchClient({
+1 -1
View File
@@ -44,7 +44,7 @@ export class SearchClient implements SearchApi {
}
async query(query: SearchQuery): Promise<SearchResultSet> {
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const queryString = qs.stringify(query);
const url = `${await this.discoveryApi.getBaseUrl(
'search/query',
+1 -1
View File
@@ -55,7 +55,7 @@ export class ProductionSentryApi implements SentryApi {
if (!this.identityApi) {
return {};
}
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
return {
headers: {
authorization: `Bearer ${token}`,
@@ -25,38 +25,16 @@ import { IdentityApi } from '@backstage/core-plugin-api';
const server = setupServer();
const identityApiAuthenticated: IdentityApi = {
getUserId() {
return 'jane-fonda';
},
getProfile() {
return { email: 'jane-fonda@spotify.com' };
},
async getIdToken() {
return Promise.resolve('fake-id-token');
},
async signOut() {
return Promise.resolve();
},
signOut: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
getCredentials: jest.fn().mockResolvedValue({ token: 'fake-id-token' }),
};
const identityApiGuest: IdentityApi = {
getUserId() {
return 'guest';
},
getProfile() {
return {};
},
async getIdToken() {
return Promise.resolve(undefined);
},
async signOut() {
return Promise.resolve();
},
signOut: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
getCredentials: jest.fn().mockResolvedValue({ token: undefined }),
};
describe('SonarQubeClient', () => {
+1 -1
View File
@@ -42,7 +42,7 @@ export class SonarQubeClient implements SonarQubeApi {
path: string,
query: { [key in string]: any },
): Promise<T | undefined> {
const idToken = await this.identityApi.getIdToken();
const { token: idToken } = await this.identityApi.getCredentials();
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sonarqube`;
const response = await fetch(
@@ -20,24 +20,15 @@ import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils';
import { splunkOnCallApiRef } from '../../api';
import { MOCK_TEAM, MOCK_INCIDENT } from '../../api/mocks';
import {
alertApiRef,
IdentityApi,
identityApiRef,
} from '@backstage/core-plugin-api';
import { alertApiRef } from '@backstage/core-plugin-api';
import { ApiProvider } from '@backstage/core-app-api';
const mockIdentityApi: Partial<IdentityApi> = {
getUserId: () => 'test',
};
const mockSplunkOnCallApi = {
getIncidents: jest.fn(),
getTeams: jest.fn(),
};
const apis = TestApiRegistry.from(
[alertApiRef, {}],
[identityApiRef, mockIdentityApi],
[splunkOnCallApiRef, mockSplunkOnCallApi],
);
@@ -59,7 +59,7 @@ export class TechInsightsClient implements TechInsightsApi {
async getAllChecks(): Promise<Check[]> {
const url = await this.discoveryApi.getBaseUrl('tech-insights');
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const response = await fetch(`${url}/checks`, {
headers: token
? {
@@ -78,7 +78,7 @@ export class TechInsightsClient implements TechInsightsApi {
checks?: Check[],
): Promise<CheckResult[]> {
const url = await this.discoveryApi.getBaseUrl('tech-insights');
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const { namespace, kind, name } = entityParams;
const checkIds = checks ? checks.map(check => check.id) : [];
const requestBody = { checks: checkIds.length > 0 ? checkIds : undefined };
@@ -106,7 +106,7 @@ export class TechInsightsClient implements TechInsightsApi {
checks?: Check[],
): Promise<BulkCheckResponse> {
const url = await this.discoveryApi.getBaseUrl('tech-insights');
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const checkIds = checks ? checks.map(check => check.id) : [];
const requestBody = {
entities,
+2 -4
View File
@@ -40,9 +40,6 @@ describe('TechDocsStorageClient', () => {
} as Partial<Config>;
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
const identityApi: jest.Mocked<IdentityApi> = {
getIdToken: jest.fn(),
getProfile: jest.fn(),
getUserId: jest.fn(),
signOut: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
@@ -51,6 +48,7 @@ describe('TechDocsStorageClient', () => {
beforeEach(() => {
jest.resetAllMocks();
identityApi.getCredentials.mockResolvedValue({ token: undefined });
});
it('should return correct base url based on defined storage', async () => {
@@ -122,7 +120,7 @@ describe('TechDocsStorageClient', () => {
},
);
identityApi.getIdToken.mockResolvedValue('token');
identityApi.getCredentials.mockResolvedValue({ token: 'token' });
await storageApi.syncEntityDocs(mockEntity);
+4 -4
View File
@@ -65,7 +65,7 @@ export class TechDocsClient implements TechDocsApi {
const apiOrigin = await this.getApiOrigin();
const requestUrl = `${apiOrigin}/metadata/techdocs/${namespace}/${kind}/${name}`;
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const request = await fetch(`${requestUrl}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
@@ -93,7 +93,7 @@ export class TechDocsClient implements TechDocsApi {
const apiOrigin = await this.getApiOrigin();
const requestUrl = `${apiOrigin}/metadata/entity/${namespace}/${kind}/${name}`;
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const request = await fetch(`${requestUrl}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
@@ -160,7 +160,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi {
const storageUrl = await this.getStorageUrl();
const url = `${storageUrl}/${namespace}/${kind}/${name}/${path}`;
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const request = await fetch(
`${url.endsWith('/') ? url : `${url}/`}index.html`,
@@ -207,7 +207,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi {
const apiOrigin = await this.getApiOrigin();
const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`;
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
return new Promise((resolve, reject) => {
// Polyfill is used to add support for custom headers and auth
+1 -1
View File
@@ -46,7 +46,7 @@ export class TodoClient implements TodoApi {
async listTodos(options: TodoListOptions): Promise<TodoListResult> {
const { entity, offset, limit, orderBy, filters } = options;
const baseUrl = await this.discoveryApi.getBaseUrl('todo');
const token = await this.identityApi.getIdToken();
const { token } = await this.identityApi.getCredentials();
const query = new URLSearchParams();
if (entity) {