Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge azure & gcp changes to external vault feature branch #8286

Merged
merged 7 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 310 additions & 21 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/insomnia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@
"@connectrpc/connect": "^1.4.0",
"@connectrpc/connect-node": "^1.4.0",
"@getinsomnia/node-libcurl": "^2.31.4",
"@google-cloud/secret-manager": "^5.6.0",
"@grpc/grpc-js": "^1.12.0",
"@grpc/proto-loader": "^0.7.13",
"@azure/msal-node": "^2.16.2",
"@seald-io/nedb": "^4.0.4",
"@segment/analytics-node": "2.1.0",
"apiconnect-wsdl": "2.0.36",
Expand All @@ -57,6 +59,7 @@
"electron-context-menu": "^3.6.1",
"electron-log": "^4.4.8",
"fastq": "^1.17.1",
"google-auth-library": "^9.15.0",
"graphql": "^16.8.1",
"graphql-ws": "^5.16.0",
"grpc-reflection-js": "jackkav/grpc-reflection-js#remove-lodash-set",
Expand Down
2 changes: 2 additions & 0 deletions packages/insomnia/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const env = process[ENV];
export const INSOMNIA_GITLAB_REDIRECT_URI = env.INSOMNIA_GITLAB_REDIRECT_URI;
export const INSOMNIA_GITLAB_CLIENT_ID = env.INSOMNIA_GITLAB_CLIENT_ID;
export const INSOMNIA_GITLAB_API_URL = env.INSOMNIA_GITLAB_API_URL;
export const INSOMNIA_AZURE_CLIENT_ID = env.INSOMNIA_AZURE_CLIENT_ID;
export const INSOMNIA_AZURE_REDIRECT_URI = env.INSOMNIA_AZURE_REDIRECT_URI;
export const PLAYWRIGHT = env.PLAYWRIGHT;
// App Stuff
export const getSkipOnboarding = () => env.INSOMNIA_SKIP_ONBOARDING;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class AWSService implements ICloudService {
this._credential = credential;
}

async authorize(): Promise<CloudServiceResult<GetCallerIdentityCommandOutput>> {
async authenticate(): Promise<CloudServiceResult<GetCallerIdentityCommandOutput>> {
const { region, accessKeyId, secretAccessKey, sessionToken } = this._credential;
const stsClient = new STSClient({
region,
Expand Down Expand Up @@ -56,9 +56,9 @@ export class AWSService implements ICloudService {
return uniqueKeyHash;
}

async getSecret(secretNameOrARN: string, config?: AWSGetSecretConfig): Promise<CloudServiceResult<GetSecretValueCommandOutput>> {
async getSecret(secretNameOrARN: string, config: AWSGetSecretConfig = {}): Promise<CloudServiceResult<GetSecretValueCommandOutput>> {
const { region, accessKeyId, secretAccessKey, sessionToken } = this._credential;
const { VersionId, VersionStage } = config || {};
const { VersionId, VersionStage } = config;
const secretClient = new SecretsManagerClient({
region,
credentials: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { type AuthenticationResult, AuthError, CryptoProvider, PublicClientApplication } from '@azure/msal-node';
import crypto from 'crypto';
import { net, shell } from 'electron';

import { INSOMNIA_AZURE_CLIENT_ID, INSOMNIA_AZURE_REDIRECT_URI, INSOMNIA_FETCH_TIME_OUT } from '../../../common/constants';
import { insomniaFetch } from '../../../ui/insomniaFetch';
import { type CloudServiceResult, type ICloudService, OAuthCloudService } from './types';

export type AzureVaultType = 'key' | 'secret';
export interface AzureGetSecretConfig {
type: AzureVaultType;
}
interface AzureSecretAttributes {
enabled: boolean;
created: number;
updated: number;
exportable: boolean;
}
export interface AzureKeyObjectResponse extends AzureSecretAttributes {
key: JsonWebKey;
}
export interface AzureSecretObjectResponse extends AzureSecretAttributes {
value: string;
contentType: string;
id: string;
}
export type AzureSecretResponse = AzureKeyObjectResponse | AzureSecretObjectResponse;

// singeleton azure client instance
let azureClient: PublicClientApplication;
let redirect_uri: string;
let verifier: string;
let challenge: string;
export const scopes = ['https://vault.azure.net/user_impersonation'];
export const azureEndpointHost = 'https://login.microsoftonline.com';
export const authority = `${azureEndpointHost}/common`;

const getAzureConfig = async () => {
// Validate and use the environment variables if provided for dev mode
if (
(INSOMNIA_AZURE_REDIRECT_URI && !INSOMNIA_AZURE_CLIENT_ID) ||
(!INSOMNIA_AZURE_REDIRECT_URI && INSOMNIA_AZURE_CLIENT_ID)
) {
throw new Error('Azure Client ID and Redirect URI must both be set.');
}

if (INSOMNIA_AZURE_CLIENT_ID && INSOMNIA_AZURE_REDIRECT_URI) {
return {
clientId: INSOMNIA_AZURE_CLIENT_ID,
redirectUri: INSOMNIA_AZURE_REDIRECT_URI,
};
}

// TODO Get Azure config from server
return insomniaFetch<{ applicationId: string; redirectUri: string; error?: string }>({
path: '/v1/oauth/azure/config',
method: 'GET',
sessionId: '',
}).then(data => {
return {
clientId: data.applicationId,
redirectUri: data.redirectUri,
};
});
};

const getAzureClient = async () => {
if (!azureClient) {
const azureConfig = await getAzureConfig();
const { clientId, redirectUri } = azureConfig;
azureClient = new PublicClientApplication({
auth: {
clientId,
authority,
},
});
redirect_uri = redirectUri;
}
return azureClient;
};

const generatePkceCodes = async () => {
const crypoProvider = new CryptoProvider();
({ verifier, challenge } = await crypoProvider.generatePkceCodes());
};
// generate Pkce Code on initialize
generatePkceCodes();

export class AzureService extends OAuthCloudService implements ICloudService {
_credential: AuthenticationResult;

constructor(credential: AuthenticationResult) {
super();
this._credential = credential;
}

static async openAuthUrl() {
const azureClient = await getAzureClient();
const authUrl = await azureClient.getAuthCodeUrl({
redirectUri: redirect_uri,
scopes,
codeChallenge: challenge,
codeChallengeMethod: 'S256',
});
// eslint-disable-next-line no-restricted-properties
shell.openExternal(authUrl);
}

static async exchangeCode({ code }: { code: string }): Promise<CloudServiceResult<AuthenticationResult>> {
const azureClient = await getAzureClient();
try {
const authResult = await azureClient.acquireTokenByCode({
scopes,
redirectUri: redirect_uri,
code,
codeVerifier: verifier,
});
// generate new Pkce codes after a success auth
await generatePkceCodes();
return {
success: true,
result: authResult,
};
} catch (error) {
if (error instanceof AuthError) {
const { errorMessage, errorCode } = error;
return {
success: false,
result: null,
error: { errorMessage, errorCode },
};
}
return {
success: false,
result: null,
error: { errorMessage: error.toString(), errorCode: '' },
};
}
}

async authenticate() {
// This method is not implemented as Azure utilizes OAuth authentication flow.
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
getUniqueCacheKey(identifier: string, _config?: any) {
const uniqueKey = identifier;
const uniqueKeyHash = crypto.createHash('md5').update(uniqueKey).digest('hex');
return uniqueKeyHash;
}

async getSecret(identifier: string): Promise<CloudServiceResult<AzureSecretResponse>> {
const { accessToken } = this._credential;
const apiVersion = '7.4';
// Using Azure rest api to get secret. Refer:
// https://learn.microsoft.com/en-us/rest/api/keyvault/secrets/get-secret/get-secret?view=rest-keyvault-secrets-7.4&tabs=HTTP
try {
const params = new URLSearchParams({
'api-version': apiVersion,
});
const secretUrl = `${identifier}?${params.toString()}`;
const requestConfig: RequestInit = {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
signal: AbortSignal.timeout(INSOMNIA_FETCH_TIME_OUT),
};
const secretResponse = await net.fetch(secretUrl, requestConfig);
if (secretResponse.ok) {
const secretBody = await secretResponse.json() as AzureSecretResponse;
return {
success: true,
result: secretBody,
};
} else {
const errorBody = await secretResponse.json() as { error?: { code: string; message: string } };
const errorMessage = errorBody.error?.message || secretResponse.statusText || 'Unknown error, failed to get secret';
return {
success: false,
result: null,
error: { errorMessage, errorCode: errorBody.error?.code || secretResponse.status.toString() },
};
}
} catch (error) {
return {
success: false,
result: null,
error: { errorMessage: error.toString(), errorCode: '' },
};
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { AuthenticationResult as AzureOAuthCredential } from '@azure/msal-node';

import * as models from '../../../models';
import type { AWSTemporaryCredential, CloudeProviderCredentialType, CloudProviderName } from '../../../models/cloud-credential';
import type { AWSTemporaryCredential, BaseCloudCredential, CloudProviderName } from '../../../models/cloud-credential';
import { ipcMainHandle, ipcMainOn } from '../electron';
import { type AWSGetSecretConfig, AWSService } from './aws-service';
import { AzureService } from './azure-service';
import { type GCPGetSecretConfig, GCPService } from './gcp-servcie';
import { type MaxAgeUnit, VaultCache } from './vault-cache';

// in-memory cache for fetched vault secrets
Expand All @@ -12,31 +16,38 @@ export interface cloudServiceBridgeAPI {
getSecret: typeof getSecret;
clearCache: typeof clearVaultCache;
setCacheMaxAge: typeof setCacheMaxAge;
openAuthUrl: typeof openAuthUrl;
exchangeCode: typeof exchangeCode;
}
export interface CloudServiceAuthOption {
provider: CloudProviderName;
credentials: CloudeProviderCredentialType;
credentials: BaseCloudCredential['credentials'];
}
export interface CloudServiceSecretOption<T extends {}> extends CloudServiceAuthOption {
secretId: string;
config?: T;
config: T;
}
export type CloudServiceGetSecretConfig = AWSGetSecretConfig;
export type CloudServiceGetSecretConfig = AWSGetSecretConfig | GCPGetSecretConfig;

export function registerCloudServiceHandlers() {
ipcMainHandle('cloudService.authenticate', (_event, options) => cspAuthentication(options));
ipcMainHandle('cloudService.getSecret', (_event, options) => getSecret(options));
ipcMainHandle('cloudService.exchangeCode', (_event, type, data) => exchangeCode(type, data));
ipcMainOn('cloudService.openAuthUrl', (_event, type) => openAuthUrl(type));
ipcMainOn('cloudService.clearCache', () => clearVaultCache());
ipcMainOn('cloudService.setCacheMaxAge', (_event, { maxAge, unit }) => setCacheMaxAge(maxAge, unit));
}

type CredentialType = AWSTemporaryCredential;
// factory pattern to create cloud service class based on its provider name
class ServiceFactory {
static createCloudService(name: CloudProviderName, credential: CredentialType) {
static createCloudService(name: CloudProviderName, credential: BaseCloudCredential['credentials']) {
switch (name) {
case 'aws':
return new AWSService(credential as AWSTemporaryCredential);
case 'azure':
return new AzureService(credential as AzureOAuthCredential);
case 'gcp':
return new GCPService(credential as string);
default:
throw new Error('Invalid cloud service provider name');
}
Expand All @@ -51,11 +62,29 @@ const setCacheMaxAge = (newAge: number, unit: MaxAgeUnit = 'min') => {
return vaultCache.setMaxAge(newAge, unit);
};

const openAuthUrl = (type: 'azure') => {
switch (type) {
case 'azure':
AzureService.openAuthUrl();
break;
default:
return;
}
};

const exchangeCode = async (type: 'azure', data: any) => {
// eslint-disable-next-line default-case
switch (type) {
case 'azure':
return AzureService.exchangeCode(data);
}
};

// authenticate with cloud service provider
const cspAuthentication = (options: CloudServiceAuthOption) => {
const { provider, credentials } = options;
const cloudService = ServiceFactory.createCloudService(provider, credentials);
return cloudService.authorize();
return cloudService.authenticate();
};

const getSecret = async (options: CloudServiceSecretOption<CloudServiceGetSecretConfig>) => {
Expand Down
Loading
Loading