From 0f838535c33cd83021f84545df00d47bf4e76c8e Mon Sep 17 00:00:00 2001 From: sneha122 Date: Wed, 8 Mar 2023 10:55:17 +0530 Subject: [PATCH] feat: file picker added and access token generation (#20778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR includes following changes: - In case of limiting google sheet access project, when user selects specific sheets as an option, they should be shown file picker UI once the authorisation is complete, In this file picker UI, users can select the google sheet files that they want to use with appsmith application and allow access to only those files. - This PR contains the changes for file picker UI and updating datasource auth state based on the files selected by user. TL;DR Steps to test this PR: - Create Google Sheet datasource - In the datasource config form, select specific sheets as an option from the scope dropdown. - Click on save and authorise - This will take you to google oauth process Screenshot 2023-02-20 at 1 24 24 PM - Select the google account - This will take you to google oauth2 consent screen Screenshot 2023-02-20 at 1 24 55 PM - Click on allow for all requested permissions - This will take you back to appsmith's datasource config page in view mode and load the file picker UI Screenshot 2023-02-20 at 1 25 47 PM - Select the files that you want to share with appsmith app - Click on select - You should see the new query button in enabled state, as datasource authorisation is complete Screenshot 2023-02-20 at 1 27 28 PM - In case you select cancel on google oauth2 consent screen, you should error message on datasource config page with new query button being disabled Screenshot 2023-02-20 at 1 28 49 PM - In case you do give all the permissions but do not select any files in google file picker, then also you should see error message on datasource config page with new query button disabled. Fixes #20163, #20290, #20160, #20162 Media > A video or a GIF is preferred. when using Loom, don’t embed because it looks like it’s a GIF. instead, just link to the video ## Type of change > Please delete options that are not relevant. - New feature (non-breaking change which adds functionality) ## How Has This Been Tested? - Manual ### Test Plan > Add Testsmith test cases links that relate to this PR ### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) ## Checklist: ### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [x] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test Co-authored-by: “sneha122” <“sneha@appsmith.com”> --- .../ServerSideTests/QueryPane/S3_1_spec.js | 14 +-- app/client/public/index.html | 12 +++ app/client/src/actions/datasourceActions.ts | 13 +++ .../src/ce/constants/ReduxActionConstants.tsx | 2 + app/client/src/entities/Datasource/index.ts | 6 ++ .../Editor/SaaSEditor/DatasourceForm.tsx | 7 ++ .../src/pages/common/datasourceAuth/index.tsx | 57 +++++++++++++ .../entityReducers/datasourceReducer.ts | 11 +++ app/client/src/sagas/DatasourcesSagas.ts | 85 ++++++++++++++----- app/client/src/selectors/editorSelectors.tsx | 3 + .../external/models/OAuthResponseDTO.java | 15 ++++ .../controllers/ce/SaasControllerCE.java | 3 +- .../solutions/ce/AuthenticationServiceCE.java | 3 +- .../ce/AuthenticationServiceCEImpl.java | 20 ++++- 14 files changed, 218 insertions(+), 33 deletions(-) create mode 100644 app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuthResponseDTO.java diff --git a/app/client/cypress/integration/Regression_TestSuite/ServerSideTests/QueryPane/S3_1_spec.js b/app/client/cypress/integration/Regression_TestSuite/ServerSideTests/QueryPane/S3_1_spec.js index 412d7c5c1e..6c0010e949 100644 --- a/app/client/cypress/integration/Regression_TestSuite/ServerSideTests/QueryPane/S3_1_spec.js +++ b/app/client/cypress/integration/Regression_TestSuite/ServerSideTests/QueryPane/S3_1_spec.js @@ -120,9 +120,9 @@ describe("Validate CRUD queries for Amazon S3 along with UI flow verifications", cy.onlyQueryRun(); cy.wait("@postExecute").then(({ response }) => { expect(response.body.data.isExecutionSuccess).to.eq(false); - expect(response.body.data.pluginErrorDetails.appsmithErrorMessage).to.contains( - "File content is not base64 encoded.", - ); + expect( + response.body.data.pluginErrorDetails.appsmithErrorMessage, + ).to.contains("File content is not base64 encoded."); }); cy.ValidateAndSelectDropdownOption( formControls.s3CreateFileDataType, @@ -253,7 +253,9 @@ describe("Validate CRUD queries for Amazon S3 along with UI flow verifications", cy.onlyQueryRun(); cy.wait("@postExecute").then(({ response }) => { expect(response.body.data.isExecutionSuccess).to.eq(false); - expect(response.body.data.pluginErrorDetails.appsmithErrorMessage).to.contain( + expect( + response.body.data.pluginErrorDetails.appsmithErrorMessage, + ).to.contain( "Your S3 query failed to execute. To know more please check the error details.", ); }); @@ -263,7 +265,9 @@ describe("Validate CRUD queries for Amazon S3 along with UI flow verifications", cy.onlyQueryRun(); cy.wait("@postExecute").then(({ response }) => { expect(response.body.data.isExecutionSuccess).to.eq(false); - expect(response.body.data.pluginErrorDetails.appsmithErrorMessage).to.contain( + expect( + response.body.data.pluginErrorDetails.appsmithErrorMessage, + ).to.contain( "Your S3 query failed to execute. To know more please check the error details.", ); }); diff --git a/app/client/public/index.html b/app/client/public/index.html index 0ef03d00ec..3f8319806a 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -46,6 +46,14 @@ } + + @@ -166,6 +174,10 @@ hideWatermark: parseConfig("__APPSMITH_HIDE_WATERMARK__"), disableIframeWidgetSandbox: parseConfig("__APPSMITH_DISABLE_IFRAME_WIDGET_SANDBOX__"), }; + + gapiLoaded = () => { + window.googleAPIsLoaded = true; + } diff --git a/app/client/src/actions/datasourceActions.ts b/app/client/src/actions/datasourceActions.ts index b388092abc..d9e061d5ea 100644 --- a/app/client/src/actions/datasourceActions.ts +++ b/app/client/src/actions/datasourceActions.ts @@ -366,6 +366,19 @@ export const initializeDatasourceFormDefaults = (pluginType: string) => { }; }; +// In case of access to specific sheets in google sheet datasource, this action +// is used for handling file picker callback, when user selects files/cancels the selection +// this callback action will be triggered +export const filePickerCallbackAction = (data: { + action: string; + datasourceId: string; +}) => { + return { + type: ReduxActionTypes.FILE_PICKER_CALLBACK_ACTION, + payload: data, + }; +}; + export default { fetchDatasources, initDatasourcePane, diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index 16188e662f..bb4625edcb 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -760,6 +760,8 @@ export const ReduxActionTypes = { AUTOLAYOUT_REORDER_WIDGETS: "AUTOLAYOUT_REORDER_WIDGETS", AUTOLAYOUT_ADD_NEW_WIDGETS: "AUTOLAYOUT_ADD_NEW_WIDGETS", RECALCULATE_COLUMNS: "RECALCULATE_COLUMNS", + SET_GSHEET_TOKEN: "SET_GSHEET_TOKEN", + FILE_PICKER_CALLBACK_ACTION: "FILE_PICKER_CALLBACK_ACTION", }; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes]; diff --git a/app/client/src/entities/Datasource/index.ts b/app/client/src/entities/Datasource/index.ts index b8bce27d78..a2b0b79fb2 100644 --- a/app/client/src/entities/Datasource/index.ts +++ b/app/client/src/entities/Datasource/index.ts @@ -11,6 +11,7 @@ export enum AuthenticationStatus { NONE = "NONE", IN_PROGRESS = "IN_PROGRESS", SUCCESS = "SUCCESS", + FAILURE = "FAILURE", } export interface DatasourceAuthentication { authType?: string; @@ -104,6 +105,11 @@ export interface Datasource extends BaseDatasource { success?: boolean; } +export interface TokenResponse { + datasource: Datasource; + token: string; +} + export interface MockDatasource { name: string; description: string; diff --git a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx index 5eb3e9fd26..37474d384d 100644 --- a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx +++ b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx @@ -34,6 +34,7 @@ import Connected from "../DataSourceEditor/Connected"; import { getCurrentApplicationId, + getGsheetToken, getPagePermissions, } from "selectors/editorSelectors"; import DatasourceAuth from "pages/common/datasourceAuth"; @@ -88,6 +89,7 @@ interface StateProps extends JSONtoFormProps { isDatasourceBeingSaved: boolean; isDatasourceBeingSavedFromPopup: boolean; isFormDirty: boolean; + gsheetToken?: string; } interface DatasourceFormFunctions { discardTempDatasource: () => void; @@ -260,6 +262,7 @@ class DatasourceSaaSEditor extends JSONtoForm { datasourceButtonConfiguration, datasourceId, formData, + gsheetToken, hiddenHeader, pageId, plugin, @@ -376,6 +379,7 @@ class DatasourceSaaSEditor extends JSONtoForm { datasourceDeleteTrigger={this.datasourceDeleteTrigger} formData={formData} getSanitizedFormData={_.memoize(this.getSanitizedData)} + gsheetToken={gsheetToken} isInvalid={this.validate()} pageId={pageId} shouldDisplayAuthMessage={!isGoogleSheetPlugin} @@ -436,6 +440,8 @@ const mapStateToProps = (state: AppState, props: any) => { ...pagePermissions, ]); + const gsheetToken = getGsheetToken(state); + return { datasource, datasourceButtonConfiguration, @@ -464,6 +470,7 @@ const mapStateToProps = (state: AppState, props: any) => { isFormDirty, canCreateDatasourceActions, featureFlags: selectFeatureFlags(state), + gsheetToken, }; }; diff --git a/app/client/src/pages/common/datasourceAuth/index.tsx b/app/client/src/pages/common/datasourceAuth/index.tsx index e8d3afa1a0..eb43c447a3 100644 --- a/app/client/src/pages/common/datasourceAuth/index.tsx +++ b/app/client/src/pages/common/datasourceAuth/index.tsx @@ -16,6 +16,7 @@ import { setDatasourceViewMode, createDatasourceFromForm, toggleSaveActionFlag, + filePickerCallbackAction, } from "actions/datasourceActions"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { getCurrentApplicationId } from "selectors/editorSelectors"; @@ -46,6 +47,7 @@ import { hasDeleteDatasourcePermission, hasManageDatasourcePermission, } from "@appsmith/utils/permissionHelpers"; +import { getAppsmithConfigs } from "ce/configs"; interface Props { datasource: Datasource; @@ -59,6 +61,7 @@ interface Props { triggerSave?: boolean; isFormDirty?: boolean; datasourceDeleteTrigger: () => void; + gsheetToken?: string; } export type DatasourceFormButtonTypes = Record; @@ -119,6 +122,7 @@ function DatasourceAuth({ shouldDisplayAuthMessage = true, triggerSave, isFormDirty, + gsheetToken, }: Props) { const authType = formData && "authType" in formData @@ -148,9 +152,18 @@ function DatasourceAuth({ const pageId = (pageIdQuery || pageIdProp) as string; const [confirmDelete, setConfirmDelete] = useState(false); + + const [scriptLoadedFlag] = useState( + (window as any).googleAPIsLoaded, + ); + const [pickerInitiated, setPickerInitiated] = useState(false); const dsName = datasource?.name; const orgId = datasource?.workspaceId; + // objects gapi and google are set, when google apis script is loaded + const gapi: any = (window as any).gapi; + const google: any = (window as any).google; + useEffect(() => { if (confirmDelete) { delayConfirmDeleteToFalse(); @@ -285,6 +298,50 @@ function DatasourceAuth({ } }; + useEffect(() => { + // This loads the picker object in gapi script + if (!!gsheetToken && !!gapi) { + gapi.load("client:picker", async () => { + await gapi.client.load( + "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", + ); + setPickerInitiated(true); + }); + } + }, [scriptLoadedFlag, gsheetToken]); + + useEffect(() => { + if (!!gsheetToken && scriptLoadedFlag && pickerInitiated && !!google) { + createPicker(gsheetToken); + } + }, [gsheetToken, scriptLoadedFlag, pickerInitiated]); + + const createPicker = async (accessToken: string) => { + const { enableGoogleOAuth } = getAppsmithConfigs(); + const googleOAuthClientId: string = enableGoogleOAuth + ""; + const APP_ID = googleOAuthClientId.split("-")[0]; + const view = new google.picker.View(google.picker.ViewId.SPREADSHEETS); + view.setMimeTypes("application/vnd.google-apps.spreadsheet"); + const picker = new google.picker.PickerBuilder() + .enableFeature(google.picker.Feature.NAV_HIDDEN) + .enableFeature(google.picker.Feature.MULTISELECT_ENABLED) + .setAppId(APP_ID) + .setOAuthToken(accessToken) + .addView(view) + .setCallback(pickerCallback) + .build(); + picker.setVisible(true); + }; + + const pickerCallback = async (data: any) => { + dispatch( + filePickerCallbackAction({ + action: data.action, + datasourceId: datasourceId, + }), + ); + }; + const createMode = datasourceId === TEMP_DATASOURCE_ID; const datasourceButtonsComponentMap = (buttonType: string): JSX.Element => { diff --git a/app/client/src/reducers/entityReducers/datasourceReducer.ts b/app/client/src/reducers/entityReducers/datasourceReducer.ts index f7cdb0597f..a1e2076b71 100644 --- a/app/client/src/reducers/entityReducers/datasourceReducer.ts +++ b/app/client/src/reducers/entityReducers/datasourceReducer.ts @@ -26,6 +26,7 @@ export interface DatasourceDataState { unconfiguredList: Datasource[]; isDatasourceBeingSaved: boolean; isDatasourceBeingSavedFromPopup: boolean; + gsheetToken: string; } const initialState: DatasourceDataState = { @@ -43,6 +44,7 @@ const initialState: DatasourceDataState = { unconfiguredList: [], isDatasourceBeingSaved: false, isDatasourceBeingSavedFromPopup: false, + gsheetToken: "", }; const datasourceReducer = createReducer(initialState, { @@ -455,6 +457,15 @@ const datasourceReducer = createReducer(initialState, { isDatasourceBeingSavedFromPopup: action.payload.isDSSavedFromPopup, }; }, + [ReduxActionTypes.SET_GSHEET_TOKEN]: ( + state: DatasourceDataState, + action: ReduxAction<{ gsheetToken: string }>, + ) => { + return { + ...state, + gsheetToken: action.payload.gsheetToken, + }; + }, }); export default datasourceReducer; diff --git a/app/client/src/sagas/DatasourcesSagas.ts b/app/client/src/sagas/DatasourcesSagas.ts index 4437a9f5bb..7f6afa1e5f 100644 --- a/app/client/src/sagas/DatasourcesSagas.ts +++ b/app/client/src/sagas/DatasourcesSagas.ts @@ -13,7 +13,7 @@ import { getFormValues, initialize, } from "redux-form"; -import _, { merge, isEmpty, get, set } from "lodash"; +import { merge, isEmpty, get, set, partition, omit } from "lodash"; import equal from "fast-deep-equal/es6"; import { ReduxAction, @@ -47,10 +47,15 @@ import { removeTempDatasource, createDatasourceSuccess, resetDefaultKeyValPairFlag, + updateDatasource, } from "actions/datasourceActions"; import { ApiResponse } from "api/ApiResponses"; import DatasourcesApi, { CreateDatasourceConfig } from "api/DatasourcesApi"; -import { Datasource } from "entities/Datasource"; +import { + AuthenticationStatus, + Datasource, + TokenResponse, +} from "entities/Datasource"; import { INTEGRATION_EDITOR_MODES, INTEGRATION_TABS } from "constants/routes"; import history from "utils/history"; @@ -332,7 +337,7 @@ function* updateDatasourceSaga( ) { try { const queryParams = getQueryParams(); - const datasourcePayload = _.omit(actionPayload.payload, "name"); + const datasourcePayload = omit(actionPayload.payload, "name"); datasourcePayload.isConfigured = true; // when clicking save button, it should be changed as configured const response: ApiResponse = yield DatasourcesApi.updateDatasource( @@ -458,7 +463,7 @@ function* getOAuthAccessTokenSaga( } try { // Get access token for datasource - const response: ApiResponse = yield OAuthApi.getAccessToken( + const response: ApiResponse = yield OAuthApi.getAccessToken( datasourceId, appsmithToken, ); @@ -466,12 +471,22 @@ function* getOAuthAccessTokenSaga( // Update the datasource object yield put({ type: ReduxActionTypes.UPDATE_DATASOURCE_SUCCESS, - payload: response.data, - }); - Toaster.show({ - text: OAUTH_AUTHORIZATION_SUCCESSFUL, - variant: Variant.success, + payload: response.data.datasource, }); + + if (!!response.data.token) { + yield put({ + type: ReduxActionTypes.SET_GSHEET_TOKEN, + payload: { + gsheetToken: response.data.token, + }, + }); + } else { + Toaster.show({ + text: OAUTH_AUTHORIZATION_SUCCESSFUL, + variant: Variant.success, + }); + } // Remove the token because it is supposed to be short lived localStorage.removeItem(APPSMITH_TOKEN_STORAGE_KEY); } @@ -702,7 +717,7 @@ function* createDatasourceFromFormSaga( formConfig, ); - const payload = _.omit(merge(initialValues, actionPayload.payload), [ + const payload = omit(merge(initialValues, actionPayload.payload), [ "id", "new", "type", @@ -778,12 +793,12 @@ function* changeDatasourceSaga( const draft: Record = yield select(getDatasourceDraft, id); const pageId: string = yield select(getCurrentPageId); let data; - if (_.isEmpty(draft)) { + if (isEmpty(draft)) { data = datasource; } else { data = draft; } - yield put(initialize(DATASOURCE_DB_FORM, _.omit(data, ["name"]))); + yield put(initialize(DATASOURCE_DB_FORM, omit(data, ["name"]))); // on reconnect modal, it shouldn't be redirected to datasource edit page if (shouldNotRedirect) return; // this redirects to the same route, so checking first. @@ -804,7 +819,7 @@ function* changeDatasourceSaga( ); yield put( // @ts-expect-error: data is of type unknown - updateReplayEntity(data.id, _.omit(data, ["name"]), ENTITY_TYPE.DATASOURCE), + updateReplayEntity(data.id, omit(data, ["name"]), ENTITY_TYPE.DATASOURCE), ); } @@ -859,10 +874,10 @@ function* storeAsDatasourceSaga() { const { values } = yield select(getFormData, API_EDITOR_FORM_NAME); const applicationId: string = yield select(getCurrentApplicationId); const pageId: string | undefined = yield select(getCurrentPageId); - let datasource = _.get(values, "datasource"); - datasource = _.omit(datasource, ["name"]); - const originalHeaders = _.get(values, "actionConfiguration.headers", []); - const [datasourceHeaders, actionHeaders] = _.partition( + let datasource = get(values, "datasource"); + datasource = omit(datasource, ["name"]); + const originalHeaders = get(values, "actionConfiguration.headers", []); + const [datasourceHeaders, actionHeaders] = partition( originalHeaders, ({ key, value }: { key: string; value: string }) => { return !(isDynamicValue(key) || isDynamicValue(value)); @@ -881,11 +896,7 @@ function* storeAsDatasourceSaga() { (d) => !(d.key === "" && d.key === ""), ); - _.set( - datasource, - "datasourceConfiguration.headers", - filteredDatasourceHeaders, - ); + set(datasource, "datasourceConfiguration.headers", filteredDatasourceHeaders); yield put(createTempDatasourceFromForm(datasource)); const createDatasourceSuccessAction: unknown = yield take( @@ -912,7 +923,7 @@ function* storeAsDatasourceSaga() { function* updateDatasourceSuccessSaga(action: UpdateDatasourceSuccessAction) { const state: AppState = yield select(); - const actionRouteInfo = _.get(state, "ui.datasourcePane.actionRouteInfo"); + const actionRouteInfo = get(state, "ui.datasourcePane.actionRouteInfo"); const generateCRUDSupportedPlugin: GenerateCRUDEnabledPluginMap = yield select( getGenerateCRUDEnabledPluginMap, ); @@ -1153,6 +1164,30 @@ function* initializeFormWithDefaults( } } +function* filePickerActionCallbackSaga( + actionPayload: ReduxAction<{ action: string; datasourceId: string }>, +) { + try { + const { action, datasourceId } = actionPayload.payload; + yield put({ + type: ReduxActionTypes.SET_GSHEET_TOKEN, + payload: { + gsheetToken: "", + }, + }); + + if (action === "cancel") { + const datasource: Datasource = yield select(getDatasource, datasourceId); + set( + datasource, + "datasourceConfiguration.authentication.authenticationStatus", + AuthenticationStatus.FAILURE, + ); + yield put(updateDatasource(datasource)); + } + } catch (error) {} +} + export function* watchDatasourcesSagas() { yield all([ takeEvery(ReduxActionTypes.FETCH_DATASOURCES_INIT, fetchDatasourcesSaga), @@ -1215,5 +1250,9 @@ export function* watchDatasourcesSagas() { takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga), takeEvery(ReduxFormActionTypes.ARRAY_PUSH, formValueChangeSaga), takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga), + takeEvery( + ReduxActionTypes.FILE_PICKER_CALLBACK_ACTION, + filePickerActionCallbackSaga, + ), ]); } diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index b46ca791ce..e94ef76d8b 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -856,3 +856,6 @@ export const showCanvasTopSectionSelector = createSelector( return true; }, ); + +export const getGsheetToken = (state: AppState) => + state.entities.datasources.gsheetToken; diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuthResponseDTO.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuthResponseDTO.java new file mode 100644 index 0000000000..9c5a9c7093 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuthResponseDTO.java @@ -0,0 +1,15 @@ +package com.appsmith.external.models; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +public class OAuthResponseDTO { + Datasource datasource; + String token; +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/SaasControllerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/SaasControllerCE.java index 7c3083ec16..81f51deaf6 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/SaasControllerCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/SaasControllerCE.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; +import com.appsmith.external.models.OAuthResponseDTO; @Slf4j @RequestMapping(Url.SAAS_URL) @@ -40,7 +41,7 @@ public class SaasControllerCE { } @PostMapping("/{datasourceId}/token") - public Mono> getAccessToken(@PathVariable String datasourceId, @RequestParam String appsmithToken, ServerWebExchange serverWebExchange) { + public Mono> getAccessToken(@PathVariable String datasourceId, @RequestParam String appsmithToken, ServerWebExchange serverWebExchange) { log.debug("Received callback for an OAuth2 authorization request"); return authenticationService.getAccessTokenFromCloud(datasourceId, appsmithToken) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCE.java index 742909568a..a3ab2053d9 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCE.java @@ -1,6 +1,7 @@ package com.appsmith.server.solutions.ce; import com.appsmith.external.models.Datasource; +import com.appsmith.external.models.OAuthResponseDTO; import com.appsmith.server.dtos.AuthorizationCodeCallbackDTO; import org.springframework.http.server.reactive.ServerHttpRequest; import reactor.core.publisher.Mono; @@ -30,7 +31,7 @@ public interface AuthenticationServiceCE { Mono getAppsmithToken(String datasourceId, String pageId, String branchName, ServerHttpRequest request, String importForGit); - Mono getAccessTokenFromCloud(String datasourceId, String appsmithToken); + Mono getAccessTokenFromCloud(String datasourceId, String appsmithToken); Mono refreshAuthentication(Datasource datasource); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCEImpl.java index 99d082bd6d..550a7bd301 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/AuthenticationServiceCEImpl.java @@ -8,6 +8,7 @@ import com.appsmith.external.helpers.SSLHelper; import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.AuthenticationResponse; import com.appsmith.external.models.Datasource; +import com.appsmith.external.models.OAuthResponseDTO; import com.appsmith.external.models.DefaultResources; import com.appsmith.external.models.OAuth2; import com.appsmith.server.configurations.CloudServicesConfig; @@ -85,6 +86,8 @@ public class AuthenticationServiceCEImpl implements AuthenticationServiceCE { private final ConfigService configService; private final DatasourcePermission datasourcePermission; private final PagePermission pagePermission; + private static final String FILE_SPECIFIC_DRIVE_SCOPE = "https://www.googleapis.com/auth/drive.file"; + private static final String ACCESS_TOKEN_KEY = "access_token"; /** * This method is used by the generic OAuth2 implementation that is used by REST APIs. Here, we only populate all the required fields @@ -385,7 +388,7 @@ public class AuthenticationServiceCEImpl implements AuthenticationServiceCE { })); } - public Mono getAccessTokenFromCloud(String datasourceId, String appsmithToken) { + public Mono getAccessTokenFromCloud(String datasourceId, String appsmithToken) { // Check if user has access to manage datasource // If yes, check if datasource is in intermediate state // If yes, request for token and store in datasource @@ -437,10 +440,21 @@ public class AuthenticationServiceCEImpl implements AuthenticationServiceCE { } } datasource.getDatasourceConfiguration().setAuthentication(oAuth2); - return Mono.just(datasource); + String accessToken = ""; + if (oAuth2.getScope() != null && oAuth2.getScope().contains(FILE_SPECIFIC_DRIVE_SCOPE)) { + accessToken = (String) tokenResponse.get(ACCESS_TOKEN_KEY); + } + return Mono.zip(Mono.just(datasource), Mono.just(accessToken)); }); }) - .flatMap(datasource -> datasourceService.update(datasource.getId(), datasource)) + .flatMap(tuple -> { + Datasource datasource = tuple.getT1(); + String accessToken = tuple.getT2(); + OAuthResponseDTO response = new OAuthResponseDTO(); + response.setDatasource(datasource); + response.setToken(accessToken); + return datasourceService.update(datasource.getId(), datasource).thenReturn(response); + }) .onErrorMap(ConnectException.class, error -> new AppsmithException( AppsmithError.AUTHENTICATION_FAILURE,