Skip to content

Commit

Permalink
Merge pull request #593 from kubeshop/f1ames/feat/synchronizer-client-id
Browse files Browse the repository at this point in the history
Pass client identity config with every API request
  • Loading branch information
f1ames authored Dec 20, 2023
2 parents 3b1c2a3 + 7126eb4 commit 978aa7a
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-cameras-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@monokle/synchronizer": minor
---

Add mechanism to pass client identity config with every API request
16 changes: 9 additions & 7 deletions packages/synchronizer/src/createMonokleAuthenticator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ApiHandler} from './handlers/apiHandler.js';
import {ApiHandler, ClientConfig} from './handlers/apiHandler.js';
import {DeviceFlowHandler} from './handlers/deviceFlowHandler.js';
import {StorageHandlerAuth} from './handlers/storageHandlerAuth.js';
import {Authenticator} from './utils/authenticator.js';
Expand All @@ -7,39 +7,41 @@ import {OriginConfig, fetchOriginConfig} from './handlers/configHandler.js';

export async function createMonokleAuthenticatorFromOrigin(
authClientId: string,
clientConfig: ClientConfig,
origin: string = DEFAULT_ORIGIN,
storageHandler: StorageHandlerAuth = new StorageHandlerAuth()
) {
try {
const originConfig = await fetchOriginConfig(origin);

return createMonokleAuthenticatorFromConfig(authClientId, originConfig, storageHandler);
return createMonokleAuthenticatorFromConfig(authClientId, clientConfig, originConfig, storageHandler);
} catch (err: any) {
throw err;
}
}

export function createMonokleAuthenticatorFromConfig(
authClientId: string,
config: OriginConfig,
clientConfig: ClientConfig,
originConfig: OriginConfig,
storageHandler: StorageHandlerAuth = new StorageHandlerAuth()
) {
if (!authClientId) {
throw new Error(`No auth clientId provided.`);
}

if (!config?.apiOrigin) {
if (!originConfig?.apiOrigin) {
throw new Error(`No api origin found in origin config from ${origin}.`);
}

if (!config?.authOrigin) {
if (!originConfig?.authOrigin) {
throw new Error(`No auth origin found in origin config from ${origin}.`);
}

return new Authenticator(
storageHandler,
new ApiHandler(config),
new DeviceFlowHandler(config.authOrigin, {
new ApiHandler(originConfig, clientConfig),
new DeviceFlowHandler(originConfig.authOrigin, {
client_id: authClientId,
client_secret: DEFAULT_DEVICE_FLOW_CLIENT_SECRET,
id_token_signed_response_alg: DEFAULT_DEVICE_FLOW_ALG,
Expand Down
12 changes: 6 additions & 6 deletions packages/synchronizer/src/createMonokleFetcher.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import {DEFAULT_ORIGIN} from './constants.js';
import {ApiHandler} from './handlers/apiHandler.js';
import {ApiHandler, ClientConfig} from './handlers/apiHandler.js';
import {OriginConfig, fetchOriginConfig} from './handlers/configHandler.js';
import {Fetcher} from './utils/fetcher.js';

export async function createMonokleFetcherFromOrigin(origin: string = DEFAULT_ORIGIN) {
export async function createMonokleFetcherFromOrigin(clientConfig: ClientConfig, origin: string = DEFAULT_ORIGIN) {
try {
const originConfig = await fetchOriginConfig(origin);

return createMonokleFetcherFromConfig(originConfig);
return createMonokleFetcherFromConfig(clientConfig, originConfig);
} catch (err: any) {
throw err;
}
}

export function createMonokleFetcherFromConfig(config: OriginConfig) {
if (!config?.apiOrigin) {
export function createMonokleFetcherFromConfig(clientConfig: ClientConfig, originConfig: OriginConfig) {
if (!originConfig?.apiOrigin) {
throw new Error(`No api origin found in origin config from ${origin}.`);
}

return new Fetcher(new ApiHandler(config));
return new Fetcher(new ApiHandler(originConfig, clientConfig));
}
12 changes: 7 additions & 5 deletions packages/synchronizer/src/createMonokleSynchronizer.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
import {DEFAULT_ORIGIN} from './constants.js';
import {ApiHandler} from './handlers/apiHandler.js';
import {ApiHandler, ClientConfig} from './handlers/apiHandler.js';
import {OriginConfig, fetchOriginConfig} from './handlers/configHandler.js';
import {GitHandler} from './handlers/gitHandler.js';
import {StorageHandlerPolicy} from './handlers/storageHandlerPolicy.js';
import {Synchronizer} from './utils/synchronizer.js';

export async function createMonokleSynchronizerFromOrigin(
clientConfig: ClientConfig,
origin: string = DEFAULT_ORIGIN,
storageHandler: StorageHandlerPolicy = new StorageHandlerPolicy(),
gitHandler: GitHandler = new GitHandler()
) {
try {
const originConfig = await fetchOriginConfig(origin);

return createMonokleSynchronizerFromConfig(originConfig, storageHandler, gitHandler);
return createMonokleSynchronizerFromConfig(clientConfig, originConfig, storageHandler, gitHandler);
} catch (err: any) {
throw err;
}
}

export function createMonokleSynchronizerFromConfig(
config: OriginConfig,
clientConfig: ClientConfig,
originConfig: OriginConfig,
storageHandler: StorageHandlerPolicy = new StorageHandlerPolicy(),
gitHandler: GitHandler = new GitHandler()
) {
if (!config?.apiOrigin) {
if (!originConfig?.apiOrigin) {
throw new Error(`No api origin found in origin config from ${origin}.`);
}

return new Synchronizer(storageHandler, new ApiHandler(config), gitHandler);
return new Synchronizer(storageHandler, new ApiHandler(originConfig, clientConfig), gitHandler);
}
36 changes: 33 additions & 3 deletions packages/synchronizer/src/handlers/apiHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,22 @@ export type ApiRepoIdData = {
};
};

export type ClientConfig = {
name: string;
version: string;
os?: string;
additionalData?: Record<string, string>;
};

export class ApiHandler {
private _apiUrl: string;
private _clientConfig: ClientConfig;
private _originConfig?: OriginConfig;

constructor();
constructor(_apiUrl: string);
constructor(_originConfig: OriginConfig);
constructor(_apiUrlOrOriginConfig: string | OriginConfig = DEFAULT_API_URL) {
constructor(_apiUrl: string, clientConfig?: ClientConfig);
constructor(_originConfig: OriginConfig, clientConfig?: ClientConfig);
constructor(_apiUrlOrOriginConfig: string | OriginConfig = DEFAULT_API_URL, clientConfig?: ClientConfig) {
if (typeof _apiUrlOrOriginConfig === 'string') {
this._apiUrl = _apiUrlOrOriginConfig;
} else if (
Expand All @@ -200,6 +208,13 @@ export class ApiHandler {
if ((this._apiUrl || '').length === 0) {
this._apiUrl = DEFAULT_API_URL;
}

this._clientConfig = {
name: clientConfig?.name || 'unknown',
version: clientConfig?.version || 'unknown',
os: clientConfig?.os || '',
additionalData: clientConfig?.additionalData || {},
};
}

get apiUrl() {
Expand Down Expand Up @@ -282,6 +297,7 @@ export class ApiHandler {
headers: {
'Content-Type': 'application/json',
Authorization: this.formatAuthorizationHeader(tokenInfo),
'User-Agent': this.formatUserAgentHeader(this._clientConfig),
},
body: JSON.stringify({
query,
Expand All @@ -298,4 +314,18 @@ export class ApiHandler {
: 'Bearer';
return `${tokenType} ${tokenInfo.accessToken}`;
}

private formatUserAgentHeader(clientConfig: ClientConfig) {
const product = `${clientConfig.name}/${clientConfig.version}`;

const comment = [];
if (clientConfig.os) {
comment.push(clientConfig.os);
}
if (clientConfig.additionalData) {
comment.push(Object.entries(clientConfig.additionalData).map(([key, value]) => `${key}=${value}`));
}

return `${product}${comment.length > 0 ? ` (${comment.join('; ')})` : ''}`;
}
}
6 changes: 4 additions & 2 deletions packages/synchronizer/src/handlers/configHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ export async function fetchOriginConfig(origin: string, timeout = 30 * 1000) {

try {
const configUrl = normalize(`${origin}/config.js`);
const response = await fetch(configUrl, { timeout });
const response = await fetch(configUrl, {timeout});

if (!response.ok) {
throw new Error(`Failed to fetch config from ${configUrl} with status ${response.status}: ${response.statusText}`);
throw new Error(
`Failed to fetch config from ${configUrl} with status ${response.status}: ${response.statusText}`
);
}

const responseText = await response.text();
Expand Down
7 changes: 5 additions & 2 deletions packages/synchronizer/src/utils/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class Authenticator extends EventEmitter {
}
}

async refreshToken(force = false, options: RefreshTokenOptions = { logoutOnInvalidGrant: true }) {
async refreshToken(force = false, options: RefreshTokenOptions = {logoutOnInvalidGrant: true}) {
const authData = this._user.data?.auth;
const tokenData = authData?.token;

Expand All @@ -104,7 +104,10 @@ export class Authenticator extends EventEmitter {
} catch (err: any) {
// This is a workaround for origin conflict where user is logged in already with different origin
// and authenticator is querying different one.
if (options?.logoutOnFail || options?.logoutOnInvalidGrant && err.message.toLowerCase().includes('invalid_grant')) {
if (
options?.logoutOnFail ||
(options?.logoutOnInvalidGrant && err.message.toLowerCase().includes('invalid_grant'))
) {
await this._storageHandler.emptyStoreData();
this._user = new User(null);
// Do not emit logout event since we treat this as user not being logged in with desired origin.
Expand Down

0 comments on commit 978aa7a

Please sign in to comment.