diff --git a/go.mod b/go.mod
index 3d1f58f0..a3aa9e4d 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index 593304f2..92f703f3 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
@@ -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=
diff --git a/migrations/000018_user_log.up.sql b/migrations/000018_user_log.up.sql
new file mode 100644
index 00000000..569f25fe
--- /dev/null
+++ b/migrations/000018_user_log.up.sql
@@ -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
+ );
\ No newline at end of file
diff --git a/proto/dekart.proto b/proto/dekart.proto
index 79f4889e..7ec53be1 100644
--- a/proto/dekart.proto
+++ b/proto/dekart.proto
@@ -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 {
@@ -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 {
diff --git a/src/client/App.js b/src/client/App.js
index ad2b9939..3e2e6d11 100644
--- a/src/client/App.js
+++ b/src/client/App.js
@@ -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 () {
@@ -41,13 +43,17 @@ function RedirectState () {
url.search = params.toString()
return // apparently receives only pathname and search
}
- return null
+ return
}
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()
@@ -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
}
@@ -73,6 +80,10 @@ function AppRedirect () {
return
}
+ if (userStream && needSensitiveScopes && !sensitiveScopesGranted) {
+ return
+ }
+
return null
}
@@ -81,6 +92,14 @@ function RedirectToSource () {
return
}
+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)
@@ -88,6 +107,12 @@ export default function App () {
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
@@ -111,12 +136,15 @@ export default function App () {
}, [dispatch])
return (
+
-
+
+
+
diff --git a/src/client/ConnectionModal.js b/src/client/ConnectionModal.js
index 4c0561c2..b87f53ac 100644
--- a/src/client/ConnectionModal.js
+++ b/src/client/ConnectionModal.js
@@ -93,7 +93,7 @@ export default function ConnectionModal () {
-
+
diff --git a/src/client/GrantScopesPage.js b/src/client/GrantScopesPage.js
new file mode 100644
index 00000000..7f3c33f8
--- /dev/null
+++ b/src/client/GrantScopesPage.js
@@ -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
+ }
+
+ if (sensitiveScopesGrantedOnce) {
+ // user will be automatically redirected to the auth page in useEffect above
+ return null
+ }
+
+ return (
+
+
+
+ }
+ title='Grant access to Google Cloud'
+ subTitle={<>Dekart needs access to your BigQuery and Google Cloud Storage to query and store results. Your token is not stored in Dekart. You can revoke access by signing out of Dekart anytime.>}
+ extra={(
+
+ )}
+ />
+