Skip to content

Commit

Permalink
Merge pull request #165 from dekart-xyz/delay-scope-request
Browse files Browse the repository at this point in the history
Delay scope request
  • Loading branch information
delfrrr committed Mar 10, 2024
2 parents 472fe74 + 774d7ed commit 8366165
Show file tree
Hide file tree
Showing 24 changed files with 890 additions and 489 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ require (
)

require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/snowflakedb/gosnowflake v1.6.22
github.com/stretchr/testify v1.8.1
golang.org/x/oauth2 v0.9.0
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 h1:BWe8a+f/t+7KY7zH2mqygeUD0t8hNFXe08p1Pb3/jKE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/GoogleCloudPlatform/golang-samples/bigquery v0.0.0-20221115172052-07ffb99455e8 h1:jEVA33EPpr9R1Uc2vhxqd+PefUOqaYy5rAzC9haWPVg=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
Expand Down Expand Up @@ -357,6 +359,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
Expand Down
6 changes: 6 additions & 0 deletions migrations/000018_user_log.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS users (
email varchar(255) PRIMARY KEY,
sensitive_scope TEXT default '', -- requested before sensitive scope
created_at timestamptz DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz DEFAULT CURRENT_TIMESTAMP
);
3 changes: 3 additions & 0 deletions proto/dekart.proto
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ message GetUserStreamResponse {
StreamOptions stream_options = 1;
int64 connection_update = 2;
string email = 3; // user email used to show user icon in UI
bool sensitive_scopes_granted = 4; // user has granted sensitive scopes
bool sensitive_scopes_granted_once = 5; // user has granted sensitive scopes at least once, now we request all scopes at once
}

message TestConnectionRequest {
Expand Down Expand Up @@ -194,6 +196,7 @@ message AuthState {
string ui_url = 3; // dekart frontend url to redirect to after auth
string access_token_to_revoke = 4; // access token to revoke
bool switch_account = 5; // if true, user will be requested to switch account
bool sensitive_scope = 6; // if true, user will be requested to grant sensitive scope
}

message ArchiveReportRequest {
Expand Down
36 changes: 32 additions & 4 deletions src/client/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { AuthState, RedirectState as DekartRedirectState } from '../proto/dekart
import { getEnv } from './actions/env'
import { authRedirect, setRedirectState } from './actions/redirect'
import { subscribeUserStream, unsubscribeUserStream } from './actions/user'
import GrantScopesPage from './GrantScopesPage'
import { loadLocalStorage } from './actions/localStorage'

// RedirectState reads states passed in the URL from the server
function RedirectState () {
Expand All @@ -41,13 +43,17 @@ function RedirectState () {
url.search = params.toString()
return <Redirect to={`${url.pathname}${url.search}`} /> // apparently receives only pathname and search
}
return null
return <AppRedirect />
}

function AppRedirect () {
const httpError = useSelector(state => state.httpError)
const { status, doNotAuthenticate } = httpError
const { newReportId } = useSelector(state => state.reportStatus)
const userStream = useSelector(state => state.user.stream)
const needSensitiveScopes = useSelector(state => state.env.needSensitiveScopes)
const sensitiveScopesGranted = userStream?.sensitiveScopesGranted
const sensitiveScopesGrantedOnce = useSelector(state => state.user.sensitiveScopesGrantedOnce)
const location = useLocation()
const dispatch = useDispatch()

Expand All @@ -56,12 +62,13 @@ function AppRedirect () {
const state = new AuthState()
state.setUiUrl(window.location.href)
state.setAction(AuthState.Action.ACTION_REQUEST_CODE)
state.setSensitiveScope(sensitiveScopesGrantedOnce) // if user has granted sensitive scopes once, request them right away without onboarding
dispatch(authRedirect(state))
}
}, [status, doNotAuthenticate, dispatch])
}, [status, doNotAuthenticate, dispatch, sensitiveScopesGrantedOnce])

if (status === 401 && doNotAuthenticate === false) {
// redirect to authentication endpoint from useEffect
// redirect to authentication endpoint from useEffect above
return null
}

Expand All @@ -73,6 +80,10 @@ function AppRedirect () {
return <Redirect to={`/reports/${newReportId}/source`} push />
}

if (userStream && needSensitiveScopes && !sensitiveScopesGranted) {
return <Redirect to='/grant-scopes' push />
}

return null
}

Expand All @@ -81,13 +92,27 @@ function RedirectToSource () {
return <Redirect to={`/reports/${id}/source`} />
}

function PageHistory ({ visitedPages }) {
const location = useLocation()
useEffect(() => {
visitedPages.current.push(location.pathname)
}, [location, visitedPages])
return null
}

export default function App () {
const errorMessage = useSelector(state => state.httpError.message)
const status = useSelector(state => state.httpError.status)
const env = useSelector(state => state.env)
const usage = useSelector(state => state.usage)
const userDefinedConnection = useSelector(state => state.connection.userDefined)
const dispatch = useDispatch()
const visitedPages = React.useRef(['/'])

useEffect(() => {
dispatch(loadLocalStorage())
}, [dispatch])

useEffect(() => {
if (window.location.pathname.startsWith('/401')) {
// do not load env and usage on 401 page
Expand All @@ -111,12 +136,15 @@ export default function App () {
}, [dispatch])
return (
<Router>
<PageHistory visitedPages={visitedPages} />
<RedirectState />
<AppRedirect />
<Switch>
<Route exact path='/'>
<HomePage reportFilter='my' />
</Route>
<Route exact path='/grant-scopes'>
<GrantScopesPage visitedPages={visitedPages} />
</Route>
<Route exact path='/shared'>
<HomePage reportFilter='discoverable' />
</Route>
Expand Down
2 changes: 1 addition & 1 deletion src/client/ConnectionModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export default function ConnectionModal () {
<Form.Item label='Connection Name' name='connectionName' required>
<Input />
</Form.Item>
<Form.Item label='Google Cloud project ID' extra='used to access BigQuery' required name='bigqueryProjectId'>
<Form.Item label='Google Cloud project ID' extra='used to bill BigQuery jobs' required name='bigqueryProjectId'>
<Input readOnly={BIGQUERY_PROJECT_ID} />
</Form.Item>
<Form.Item label='Google Cloud Storage bucket' extra='where queries, files and query results stored' required name='cloudStorageBucket'>
Expand Down
67 changes: 67 additions & 0 deletions src/client/GrantScopesPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Header } from './Header'
import styles from './GrantScopesPage.module.css'
import Result from 'antd/es/result'
import Button from 'antd/es/button'
import { useDispatch, useSelector } from 'react-redux'
import { requestSensitiveScopes } from './actions/redirect'
import { CloudTwoTone } from '@ant-design/icons'
import { useEffect } from 'react'
import { Redirect } from 'react-router-dom/cjs/react-router-dom'

function getLastPage (visitedPages) {
return visitedPages.current.filter(page => page !== '/grant-scopes').pop()
}

export default function GrantScopesPage ({ visitedPages }) {
const dispatch = useDispatch()
const userStream = useSelector(state => state.user.stream)
const sensitiveScopesGrantedOnce = useSelector(state => state.user.sensitiveScopesGrantedOnce)

useEffect(() => {
if (
!userStream || // userStream is not yet loaded
userStream.sensitiveScopesGranted // user has already granted sensitive scopes
) {
return
}
if (sensitiveScopesGrantedOnce) { // user has granted sensitive scopes once, request them right away without onboarding
dispatch(requestSensitiveScopes(getLastPage(visitedPages)))
}
}
, [dispatch, userStream, visitedPages, sensitiveScopesGrantedOnce])

if (!userStream) {
return null
}

if (userStream.sensitiveScopesGranted) {
// user shouldn't be here
return <Redirect to='/' push />
}

if (sensitiveScopesGrantedOnce) {
// user will be automatically redirected to the auth page in useEffect above
return null
}

return (
<div className={styles.grantScopesPage}>
<Header />
<div className={styles.body}>
<Result
icon={<CloudTwoTone />}
title='Grant access to Google Cloud'
subTitle={<>Dekart needs access to your <b>BigQuery</b> and <b>Google Cloud Storage</b> to query and store results.<br /> Your token is not stored in Dekart. You can revoke access by signing out of Dekart anytime.</>}
extra={(
<Button
type='primary' onClick={() => {
dispatch(requestSensitiveScopes(getLastPage(visitedPages)))
}}
>Continue to Google
</Button>
)}
/>
</div>
</div>
)
}
17 changes: 17 additions & 0 deletions src/client/GrantScopesPage.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.grantScopesPage {
width: 100%;
height: 100%;
position: absolute;
background-color: #F7F7F7;
display: flex;
flex-direction: column;
}

.body {
padding: 20px;
display: flex;
flex: 1;
overflow: scroll;
align-items: center;
flex-direction: column;
}
8 changes: 4 additions & 4 deletions src/client/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ function getSignature (email) {

function User ({ buttonDivider }) {
const token = useSelector(state => state.token)
const user = useSelector(state => state.user)
const userStream = useSelector(state => state.user.stream)
const dispatch = useDispatch()
if (!user || !token) {
if (!userStream || !token) {
return null
}
return (
Expand All @@ -37,7 +37,7 @@ function User ({ buttonDivider }) {
overlayClassName={styles.userDropdown} menu={{
items: [
{
label: user && user.email,
label: userStream && userStream.email,
disabled: true
},
{
Expand All @@ -62,7 +62,7 @@ function User ({ buttonDivider }) {
}
]
}}
><Avatar>{getSignature(user && user.email)}</Avatar>
><Avatar>{getSignature(userStream && userStream.email)}</Avatar>
</Dropdown>

</div>
Expand Down
2 changes: 1 addition & 1 deletion src/client/HomePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ function FirstConnectionOnboarding () {
status='success'
icon={<ApiTwoTone />}
title='Ready to connect!'
subTitle='Before you can create a map, you need to connect to your Google Cloud account.'
subTitle={<>Next step, select <b>Project ID</b> for BigQuery billing and <b>Storage Bucket</b> name to store your query results.</>}
extra={<Button type='primary' onClick={() => { dispatch(newConnection()) }}>Create connection</Button>}
/>
</>
Expand Down
10 changes: 9 additions & 1 deletion src/client/actions/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,19 @@ export function archiveConnection (id) {
}
}

function getDefaultName (connectionsList, suffix = 0) {
const name = suffix ? `BigQuery (${suffix})` : 'BigQuery'
if (connectionsList.find(c => c.connectionName === name)) {
return getDefaultName(connectionsList, suffix + 1)
}
return name
}

export function newConnection (datasetId) {
return async (dispatch, getState) => {
dispatch({ type: newConnection.name })

const connectionName = `Untitled connection ${(new Date()).toLocaleString()}`
const connectionName = getDefaultName(getState().connection.list)
const request = new CreateConnectionRequest()
request.setConnectionName(connectionName)

Expand Down
34 changes: 34 additions & 0 deletions src/client/actions/localStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

const LOCAL_STORAGE_KEY = 'dekart-local-storage-v1'

const initialState = {
sensitiveScopesGrantedOnce: false
}

let current = initialState

export function localStorageInit () {
return {
type: localStorageInit.name,
current
}
}

export function updateLocalStorage (key, value) {
current[key] = value
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(current))
return {
type: updateLocalStorage.name,
current
}
}

export function loadLocalStorage () {
return (dispatch) => {
const localStorageValue = window.localStorage.getItem(LOCAL_STORAGE_KEY)
if (localStorageValue) {
current = JSON.parse(localStorageValue)
dispatch(localStorageInit())
}
}
}
13 changes: 13 additions & 0 deletions src/client/actions/redirect.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { AuthState } from '../../proto/dekart_pb'

export function setRedirectState (redirectState) {
return { type: setRedirectState.name, redirectState }
}

export function requestSensitiveScopes (returnPath) {
return async (dispatch) => {
const url = new URL(window.location.href)
url.pathname = returnPath
const state = new AuthState()
state.setUiUrl(url.href)
state.setAction(AuthState.Action.ACTION_REQUEST_CODE)
state.setSensitiveScope(true)
dispatch(authRedirect(state))
}
}

const { REACT_APP_API_HOST } = process.env // this never changes, passed during build

// authRedirect will redirect the browser to the authentication endpoint
Expand Down
2 changes: 2 additions & 0 deletions src/client/actions/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { GetUserStreamRequest } from '../../proto/dekart_pb'
import { Dekart } from '../../proto/dekart_pb_service'
import { getConnectionsList } from './connection'
import { grpcStream, grpcStreamCancel } from './grpc'
import { updateLocalStorage } from './localStorage'

export function userStreamUpdate (userStream) {
return {
Expand All @@ -19,6 +20,7 @@ export function subscribeUserStream () {
}
dispatch(grpcStream(Dekart.GetUserStream, request, (message, err) => {
if (message) {
dispatch(updateLocalStorage('sensitiveScopesGrantedOnce', message.sensitiveScopesGrantedOnce))
dispatch(userStreamUpdate(message))
if (prevRes.connectionUpdate !== message.connectionUpdate) {
prevRes.connectionUpdate = message.connectionUpdate
Expand Down
3 changes: 2 additions & 1 deletion src/client/reducers/rootReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ function env (state = defaultEnv, action) {
: action.variables.REQUIRE_GOOGLE_OAUTH
? 'GOOGLE_OAUTH'
: 'NONE'
)
),
needSensitiveScopes: action.variables.REQUIRE_GOOGLE_OAUTH
}
default:
return state
Expand Down
Loading

0 comments on commit 8366165

Please sign in to comment.