Skip to content

POC-Ri 6295 entra id #4183

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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
"yarn-deduplicate": "^3.1.0"
},
"dependencies": {
"@azure/msal-node": "^2.16.2",
"@elastic/datemath": "^5.0.3",
"@elastic/eui": "34.6.0",
"@reduxjs/toolkit": "^1.6.2",
Expand Down
5 changes: 5 additions & 0 deletions redisinsight/api/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ export default {
databaseConnectionTimeout: parseInt(process.env.RI_CLOUD_DATABASE_CONNECTION_TIMEOUT, 10) || 30 * 1000,
renewTokensBeforeExpire: parseInt(process.env.RI_CLOUD_DATABASE_CONNECTION_TIMEOUT, 10) || 2 * 60_000, // 2min
idp: {
microsoft: {
redirectUri: process.env.RI_CLOUD_IDP_MICROSOFT_REDIRECT_URI || process.env.RI_CLOUD_IDP_REDIRECT_URI,
clientId: process.env.RI_CLOUD_IDP_MICROSOFT_CLIENT_ID || process.env.RI_CLOUD_IDP_CLIENT_ID,
authority: process.env.RI_CLOUD_IDP_MICROSOFT_AUTHORITY,
},
google: {
authorizeUrl: process.env.RI_CLOUD_IDP_GOOGLE_AUTHORIZE_URL || process.env.RI_CLOUD_IDP_AUTHORIZE_URL,
tokenUrl: process.env.RI_CLOUD_IDP_GOOGLE_TOKEN_URL || process.env.RI_CLOUD_IDP_TOKEN_URL,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const { PublicClientApplication, CryptoProvider } = require('@azure/msal-node');
import { Logger } from '@nestjs/common';
import config from 'src/utils/config';
import { CloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/cloud-auth.strategy';
import { CloudAuthIdpType, CloudAuthRequest, CloudAuthRequestOptions } from 'src/modules/cloud/auth/models/cloud-auth-request';
import { SessionMetadata } from 'src/common/models';
import { plainToClass } from 'class-transformer';

const { idp: { microsoft: idpConfig } } = config.get('cloud');

export class MicrosoftIdpCloudAuthStrategy extends CloudAuthStrategy {
private logger = new Logger('MicrosoftIdpCloudAuthStrategy');
private cryptoProvider: typeof CryptoProvider;
private msalClient: typeof PublicClientApplication;

constructor() {
super();

this.cryptoProvider = new CryptoProvider();
this.msalClient = new PublicClientApplication({
auth: {
clientId: idpConfig.clientId,
authority: idpConfig.authority,
}
});

this.config = {
idpType: CloudAuthIdpType.Microsoft,
authorizeUrl: `${idpConfig.authority}/oauth2/v2.0/authorize`,
tokenUrl: `${idpConfig.authority}/oauth2/v2.0/token`,
revokeTokenUrl: `${idpConfig.authority}/oauth2/v2.0/logout`,
clientId: idpConfig.clientId,
pkce: true,
redirectUri: idpConfig.redirectUri,
scopes: ['offline_access', 'openid', 'email', 'profile'],
responseMode: 'query',
responseType: 'code'
};
}

async generateAuthRequest(
sessionMetadata: SessionMetadata,
options?: CloudAuthRequestOptions,
): Promise<CloudAuthRequest> {
const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();

const state = this.cryptoProvider.createNewGuid();
const nonce = this.cryptoProvider.createNewGuid();

return plainToClass(CloudAuthRequest, {
...this.config,
state,
nonce,
codeVerifier: verifier,
codeChallenge: challenge,
codeChallengeMethod: 'S256',
sessionMetadata,
createdAt: new Date(),
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { CloudAuthService } from 'src/modules/cloud/auth/cloud-auth.service';
import { CloudAuthController } from 'src/modules/cloud/auth/cloud-auth.controller';
import { CloudAuthAnalytics } from 'src/modules/cloud/auth/cloud-auth.analytics';
import { TcpCloudAuthStrategy } from './auth-strategy/tcp-cloud.auth.strategy';
import { MicrosoftIdpCloudAuthStrategy } from './auth-strategy/microsoft-idp.cloud.auth-strategy';

@Module({
imports: [CloudSessionModule],
providers: [
GoogleIdpCloudAuthStrategy,
MicrosoftIdpCloudAuthStrategy,
GithubIdpCloudAuthStrategy,
SsoIdpCloudAuthStrategy,
CloudAuthService,
Expand Down
141 changes: 138 additions & 3 deletions redisinsight/api/src/modules/cloud/auth/cloud-auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Injectable, Logger } from '@nestjs/common';
import log from 'electron-log'
import axios from 'axios';
import { GoogleIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/google-idp.cloud.auth-strategy';
import { MicrosoftIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/microsoft-idp.cloud.auth-strategy';
import {
CloudAuthIdpType,
CloudAuthRequest,
Expand Down Expand Up @@ -40,6 +42,7 @@ export class CloudAuthService {
constructor(
private readonly sessionService: CloudSessionService,
private readonly googleIdpAuthStrategy: GoogleIdpCloudAuthStrategy,
private readonly microsoftIdpAuthStrategy: MicrosoftIdpCloudAuthStrategy,
private readonly githubIdpCloudAuthStrategy: GithubIdpCloudAuthStrategy,
private readonly ssoIdpCloudAuthStrategy: SsoIdpCloudAuthStrategy,
private readonly analytics: CloudAuthAnalytics,
Expand Down Expand Up @@ -86,6 +89,8 @@ export class CloudAuthService {
switch (strategy) {
case CloudAuthIdpType.Google:
return this.googleIdpAuthStrategy;
case CloudAuthIdpType.Microsoft:
return this.microsoftIdpAuthStrategy;
case CloudAuthIdpType.GitHub:
return this.githubIdpCloudAuthStrategy;
case CloudAuthIdpType.Sso:
Expand Down Expand Up @@ -133,17 +138,147 @@ export class CloudAuthService {
*/
private async exchangeCode(authRequest: CloudAuthRequest, code: string): Promise<any> {
try {
// Step 1: Exchange authorization code for tokens
const tokenUrl = CloudAuthStrategy.generateExchangeCodeUrl(authRequest, code);

const { data } = await axios.post(tokenUrl.toString().split('?')[0], tokenUrl.searchParams, {
headers: CloudAuthService.getOAuthHttpRequestHeaders(),
});

return data;
if (authRequest.idpType === CloudAuthIdpType.Microsoft && data.access_token) {
const accessToken = data.access_token;

// Step 2: List Azure Subscriptions
const listAzureSubscriptions = async (): Promise<any[]> => {
try {
const url = `https://management.azure.com/subscriptions?api-version=2024-08-01`;
const response = await axios.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
this.logger.log('Subscriptions found:', response.data.value);
return response.data.value;
} catch (e) {
this.logger.error('Failed to fetch Azure subscriptions', e.response?.data || e.message);
throw wrapHttpError(e);
}
};

const subscriptions = await listAzureSubscriptions();

// Step 3: For each subscription, fetch Resource Groups and Redis instances
const listResourceGroups = async (subscriptionId: string): Promise<any[]> => {
try {
const url = `https://management.azure.com/subscriptions/${subscriptionId}/resourcegroups?api-version=2024-08-01`;
const response = await axios.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response.data.value;
} catch (e) {
this.logger.error('Failed to fetch Resource Groups', e.response?.data || e.message);
throw wrapHttpError(e);
}
};

// Step 4: Get access keys for each Redis instance. Access key is used as a password for the authentication with the current auth implementation
const getRedisInstanceAccessKeys = async (
subscriptionId: string,
resourceGroupName: string,
redisInstanceName: string
): Promise<any[]> => {
try {
const keysUrl = `https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Cache/Redis/${redisInstanceName}/listKeys?api-version=2024-11-01`;
const keysResponse = await axios.post(keysUrl, null, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});

return keysResponse.data.primaryKey
} catch (e) {
this.logger.error(`Failed to fetch keys for Redis instance ${redisInstanceName}`, e.response?.data || e.message);
return null;
}
};

const listAzureRedisDatabases = async (
subscriptionId: string,
resourceGroupName: string
): Promise<any[]> => {
try {
// Get Redis instances list
const url = `https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Cache/Redis?api-version=2024-11-01`;
const response = await axios.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});

// For each Redis instance, get access keys
const redisInstances = await Promise.all(response.data.value.map(async (instance) => {
const accessKey = await getRedisInstanceAccessKeys(subscriptionId, resourceGroupName, instance.name)
return {
...instance,
connectionDetails: {
hostName: instance.properties.hostName,
port: instance.properties.sslPort, // might need to check additionally if there are other available ports
accessKey,
username: instance.properties.userName || 'default',
}
}
}))

return redisInstances;
} catch (e) {
console.log('Failed to fetch Redis Databases', subscriptionId, resourceGroupName, e.response?.data || e.message);
// removed throwing for the POC because we are going over ALL resources and don't want to stop due to 1 error
// throw wrapHttpError(e);
}
};

const results: any[] = [];

for (const subscription of subscriptions) {
const subscriptionId = subscription.subscriptionId;
const resourceGroups = await listResourceGroups(subscriptionId);

for (const resourceGroup of resourceGroups) {
const redisInstances = await listAzureRedisDatabases(subscriptionId, resourceGroup.name);
// Only add to results if redisInstances exists and has length
if (redisInstances?.length > 0) {
results.push({
subscriptionId,
resourceGroup: resourceGroup.name,
redisInstances,
});
}
}
}

// Log only results with Redis instances
results.forEach(result => {
console.log('Azure Redis Databases result', result);
result.redisInstances.forEach(redisInstance => {
console.log('Azure Redis Database instance details:', {
name: redisInstance.name,
connectionDetails: redisInstance.connectionDetails,
properties: redisInstance.properties
});
});
});

return {
accessToken,
redisData: results,
};
}

return data; // Default return if idpType is not Microsoft
} catch (e) {
this.logger.error('Unable to exchange code', e);

// todo: handle this?
throw wrapHttpError(e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SessionMetadata } from 'src/common/models';

export enum CloudAuthIdpType {
Google = 'google',
Microsoft = 'microsoft',
GitHub = 'github',
Sso = 'sso',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ const OAuthUserProfile = (props: Props) => {
if (!data) {
if (server?.packageType === PackageType.Mas) return null

if (initialLoading) {
// hardcoded skip for testing
if (initialLoading && false) {
return (
<div className={styles.loadingContainer}>
<EuiLoadingSpinner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const OAuthForm = ({
switch (authStrategy) {
case OAuthStrategy.Google:
case OAuthStrategy.GitHub:
case OAuthStrategy.Microsoft:
initOAuthProcess(authStrategy, action)
break
case OAuthStrategy.SSO:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React from 'react'
import { EuiButtonEmpty, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'
import cx from 'classnames'
import { useSelector } from 'react-redux'
Expand All @@ -24,6 +24,14 @@ const OAuthSocialButtons = (props: Props) => {
const agreement = useSelector(oauthCloudPAgreementSelector)

const socialLinks = [
{
text: 'Microsoft',
className: styles.microsoftButton,
// reusing the sso icon for microsoft for testing
icon: SsoIcon,
label: 'microsoft-oauth',
strategy: OAuthStrategy.Microsoft,
},
{
text: 'Google',
className: styles.googleButton,
Expand Down
11 changes: 6 additions & 5 deletions redisinsight/ui/src/components/page-header/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,12 @@ const PageHeader = (props: Props) => {
</EuiFlexItem>
)}
<EuiFlexItem><InsightsTrigger source="home page" /></EuiFlexItem>
<FeatureFlagComponent name={FeatureFlags.cloudSso}>
<EuiFlexItem style={{ marginLeft: 16 }}>
<OAuthUserProfile source={OAuthSocialSource.UserProfile} />
</EuiFlexItem>
</FeatureFlagComponent>
{/* hardcoded skip for testing */}
{/* <FeatureFlagComponent name={FeatureFlags.cloudSso}> */}
<EuiFlexItem style={{ marginLeft: 16 }}>
<OAuthUserProfile source={OAuthSocialSource.UserProfile} />
</EuiFlexItem>
{/* </FeatureFlagComponent> */}
</EuiFlexGroup>
) : (
<div className={styles.pageHeaderLogo}>
Expand Down
1 change: 1 addition & 0 deletions redisinsight/ui/src/slices/interfaces/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export enum OAuthSocialAction {
export enum OAuthStrategy {
Google = 'google',
GitHub = 'github',
Microsoft = 'microsoft',
SSO = 'sso'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ const HomePageTemplate = (props: Props) => {
</EuiFlexItem>
)}
<EuiFlexItem><InsightsTrigger source="home page" /></EuiFlexItem>
<FeatureFlagComponent name={FeatureFlags.cloudSso}>
<EuiFlexItem style={{ marginLeft: 16 }}>
<OAuthUserProfile source={OAuthSocialSource.UserProfile} />
</EuiFlexItem>
</FeatureFlagComponent>
{/* hardcoded skip for testing */}
{/* <FeatureFlagComponent name={FeatureFlags.cloudSso}> */}
<EuiFlexItem style={{ marginLeft: 16 }}>
<OAuthUserProfile source={OAuthSocialSource.UserProfile} />
</EuiFlexItem>
{/* </FeatureFlagComponent> */}
</EuiFlexGroup>
</div>
<div className={styles.pageWrapper}>
Expand Down
Loading