Fix google compose scope not gated by feature flag (#18093)
## Context New google api scope has been introduced in https://github.com/twentyhq/twenty/pull/17793 without being gated behind a feature flag Microsoft is using pre-existing Mail.ReadWrite scope there is nothing to gate ## Before <img width="553" height="445" alt="Screenshot 2026-02-19 at 14 57 58" src="https://github.com/user-attachments/assets/59a4f76b-d38d-492f-b013-b6cad4091a7f" /> ## After <img width="535" height="392" alt="Screenshot 2026-02-19 at 14 58 44" src="https://github.com/user-attachments/assets/0337bf15-ec30-4549-bb9d-571a982dffd8" />
This commit is contained in:
+18
-1
@@ -12,6 +12,8 @@ import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-mod
|
||||
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
|
||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -27,6 +29,7 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly workspaceDomainsService: WorkspaceDomainsService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -46,7 +49,21 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
);
|
||||
}
|
||||
|
||||
new GoogleAPIsOauthExchangeCodeForTokenStrategy(this.twentyConfigService);
|
||||
const { workspaceId } =
|
||||
await this.transientTokenService.verifyTransientToken(
|
||||
state.transientToken,
|
||||
);
|
||||
|
||||
const isDraftEmailEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_DRAFT_EMAIL_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
new GoogleAPIsOauthExchangeCodeForTokenStrategy(
|
||||
this.twentyConfigService,
|
||||
isDraftEmailEnabled,
|
||||
);
|
||||
|
||||
setRequestExtraParams(request, {
|
||||
transientToken: state.transientToken,
|
||||
|
||||
+13
-1
@@ -12,6 +12,8 @@ import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth
|
||||
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
|
||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -25,6 +27,7 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly workspaceDomainsService: WorkspaceDomainsService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
super({
|
||||
prompt: 'select_account',
|
||||
@@ -68,7 +71,16 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
|
||||
);
|
||||
}
|
||||
|
||||
new GoogleAPIsOauthRequestCodeStrategy(this.twentyConfigService);
|
||||
const isDraftEmailEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_DRAFT_EMAIL_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
new GoogleAPIsOauthRequestCodeStrategy(
|
||||
this.twentyConfigService,
|
||||
isDraftEmailEnabled,
|
||||
);
|
||||
|
||||
return (await super.canActivate(context)) as boolean;
|
||||
} catch (err) {
|
||||
|
||||
+19
-3
@@ -92,8 +92,24 @@ describe('GoogleAPIScopesService', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should work with the current Google API scopes', () => {
|
||||
// What is currently returned by Google
|
||||
it('should work with the current Google API scopes when draft email is disabled', () => {
|
||||
const actualGoogleScopes = [
|
||||
'https://www.googleapis.com/auth/calendar.events',
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/profile.emails.read',
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'openid',
|
||||
];
|
||||
const expectedScopes = getGoogleApisOauthScopes(false);
|
||||
|
||||
const result = includesExpectedScopes(actualGoogleScopes, expectedScopes);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should work with the current Google API scopes when draft email is enabled', () => {
|
||||
const actualGoogleScopes = [
|
||||
'https://www.googleapis.com/auth/calendar.events',
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
@@ -104,7 +120,7 @@ describe('GoogleAPIScopesService', () => {
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'openid',
|
||||
];
|
||||
const expectedScopes = getGoogleApisOauthScopes();
|
||||
const expectedScopes = getGoogleApisOauthScopes(true);
|
||||
|
||||
const result = includesExpectedScopes(actualGoogleScopes, expectedScopes);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export class GoogleAPIScopesService {
|
||||
|
||||
public async getScopesFromGoogleAccessTokenAndCheckIfExpectedScopesArePresent(
|
||||
accessToken: string,
|
||||
isDraftEmailEnabled = false,
|
||||
): Promise<{ scopes: string[]; isValid: boolean }> {
|
||||
try {
|
||||
const httpClient = this.secureHttpClientService.getHttpClient();
|
||||
@@ -40,7 +41,7 @@ export class GoogleAPIScopesService {
|
||||
);
|
||||
|
||||
const scopes = response.data.scope.split(' ');
|
||||
const expectedScopes = getGoogleApisOauthScopes();
|
||||
const expectedScopes = getGoogleApisOauthScopes(isDraftEmailEnabled);
|
||||
|
||||
return {
|
||||
scopes,
|
||||
|
||||
+7
@@ -10,6 +10,7 @@ import { GoogleAPIScopesService } from 'src/engine/core-modules/auth/services/go
|
||||
import { GoogleApisServiceAvailabilityService } from 'src/engine/core-modules/auth/services/google-apis-service-availability.service';
|
||||
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
|
||||
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@@ -181,6 +182,12 @@ describe('GoogleAPIsService', () => {
|
||||
provide: getQueueToken(MessageQueue.calendarQueue),
|
||||
useValue: mockCalendarQueueService,
|
||||
},
|
||||
{
|
||||
provide: FeatureFlagService,
|
||||
useValue: {
|
||||
isFeatureEnabled: jest.fn().mockResolvedValue(false),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import { CreateMessageChannelService } from 'src/engine/core-modules/auth/servic
|
||||
import { GoogleAPIScopesService } from 'src/engine/core-modules/auth/services/google-apis-scopes';
|
||||
import { GoogleApisServiceAvailabilityService } from 'src/engine/core-modules/auth/services/google-apis-service-availability.service';
|
||||
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
@@ -63,6 +65,7 @@ export class GoogleAPIsService {
|
||||
private readonly updateConnectedAccountOnReconnectService: UpdateConnectedAccountOnReconnectService,
|
||||
private readonly googleAPIScopesService: GoogleAPIScopesService,
|
||||
private readonly googleApisServiceAvailabilityService: GoogleApisServiceAvailabilityService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
async refreshGoogleRefreshToken(input: {
|
||||
@@ -92,9 +95,15 @@ export class GoogleAPIsService {
|
||||
'MESSAGING_PROVIDER_GMAIL_ENABLED',
|
||||
);
|
||||
|
||||
const isDraftEmailEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_DRAFT_EMAIL_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const { scopes, isValid } =
|
||||
await this.googleAPIScopesService.getScopesFromGoogleAccessTokenAndCheckIfExpectedScopesArePresent(
|
||||
input.accessToken,
|
||||
isDraftEmailEnabled,
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
|
||||
+5
-2
@@ -14,8 +14,11 @@ export abstract class GoogleAPIsOauthCommonStrategy extends PassportStrategy(
|
||||
Strategy,
|
||||
'google-apis',
|
||||
) {
|
||||
constructor(twentyConfigService: TwentyConfigService) {
|
||||
const scopes = getGoogleApisOauthScopes();
|
||||
constructor(
|
||||
twentyConfigService: TwentyConfigService,
|
||||
isDraftEmailEnabled = false,
|
||||
) {
|
||||
const scopes = getGoogleApisOauthScopes(isDraftEmailEnabled);
|
||||
|
||||
super({
|
||||
clientID: twentyConfigService.get('AUTH_GOOGLE_CLIENT_ID'),
|
||||
|
||||
+5
-2
@@ -12,8 +12,11 @@ export type GoogleAPIScopeConfig = {
|
||||
|
||||
@Injectable()
|
||||
export class GoogleAPIsOauthExchangeCodeForTokenStrategy extends GoogleAPIsOauthCommonStrategy {
|
||||
constructor(twentyConfigService: TwentyConfigService) {
|
||||
super(twentyConfigService);
|
||||
constructor(
|
||||
twentyConfigService: TwentyConfigService,
|
||||
isDraftEmailEnabled = false,
|
||||
) {
|
||||
super(twentyConfigService, isDraftEmailEnabled);
|
||||
}
|
||||
|
||||
async validate(
|
||||
|
||||
+5
-2
@@ -12,8 +12,11 @@ export type GoogleAPIScopeConfig = {
|
||||
|
||||
@Injectable()
|
||||
export class GoogleAPIsOauthRequestCodeStrategy extends GoogleAPIsOauthCommonStrategy {
|
||||
constructor(twentyConfigService: TwentyConfigService) {
|
||||
super(twentyConfigService);
|
||||
constructor(
|
||||
twentyConfigService: TwentyConfigService,
|
||||
isDraftEmailEnabled = false,
|
||||
) {
|
||||
super(twentyConfigService, isDraftEmailEnabled);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
/** email, profile and openid permission can be called without the https://www.googleapis.com/auth/ prefix
|
||||
* see https://developers.google.com/identity/protocols/oauth2/scopes
|
||||
*/
|
||||
export const getGoogleApisOauthScopes = () => {
|
||||
export const getGoogleApisOauthScopes = (isDraftEmailEnabled = false) => {
|
||||
return [
|
||||
'email',
|
||||
'profile',
|
||||
@@ -9,6 +9,8 @@ export const getGoogleApisOauthScopes = () => {
|
||||
'https://www.googleapis.com/auth/calendar.events',
|
||||
'https://www.googleapis.com/auth/profile.emails.read',
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.compose',
|
||||
...(isDraftEmailEnabled
|
||||
? ['https://www.googleapis.com/auth/gmail.compose']
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user