This is an automated email from the ASF dual-hosted git repository.

lauraxia pushed a commit to branch simple-login-v1.1.0
in repository https://gitbox.apache.org/repos/asf/gravitino.git

commit bccb7b3866e3f787fd218c13939143e318e0ddcc
Author: Qian Xia <[email protected]>
AuthorDate: Wed Jan 7 18:55:20 2026 +0800

    support simple type login
---
 web/web/src/app/login/components/DefaultLogin.js   | 223 ++++++++++++---------
 .../src/app/login/components/defaultLoginSchema.js |  48 +++++
 web/web/src/app/rootLayout/Logout.js               |  14 +-
 web/web/src/lib/auth/providers/generic.js          |   8 +
 web/web/src/lib/provider/session.js                |  33 ++-
 web/web/src/lib/store/auth/index.js                |  57 ++++--
 web/web/src/lib/utils/axios/index.js               |   7 +
 7 files changed, 266 insertions(+), 124 deletions(-)

diff --git a/web/web/src/app/login/components/DefaultLogin.js 
b/web/web/src/app/login/components/DefaultLogin.js
index fd5ff0debf..2ee598a291 100644
--- a/web/web/src/app/login/components/DefaultLogin.js
+++ b/web/web/src/app/login/components/DefaultLogin.js
@@ -20,41 +20,31 @@
 'use client'
 
 import { useRouter } from 'next/navigation'
-import { useEffect } from 'react'
+import { useEffect, useMemo } from 'react'
 import { Grid, Button, Typography, TextField, FormControl, FormHelperText } 
from '@mui/material'
-import * as yup from 'yup'
 import { useForm, Controller } from 'react-hook-form'
 import { yupResolver } from '@hookform/resolvers/yup'
 
 import { useAppDispatch, useAppSelector } from '@/lib/hooks/useStore'
-import { loginAction, setIntervalIdAction, clearIntervalId } from 
'@/lib/store/auth'
-
-const defaultValues = {
-  grant_type: 'client_credentials',
-  client_id: '',
-  client_secret: '',
-  scope: ''
-}
-
-const schema = yup.object().shape({
-  grant_type: yup.string().required(),
-  client_id: yup.string().required(),
-  client_secret: yup.string().required(),
-  scope: yup.string().required()
-})
+import { loginAction, setIntervalIdAction, clearIntervalId, setAuthUser } from 
'@/lib/store/auth'
+import { DEFAULT_LOGIN_DEFAULT_VALUES, createDefaultLoginSchema } from 
'./defaultLoginSchema'
 
 function DefaultLogin() {
   const router = useRouter()
   const dispatch = useAppDispatch()
   const store = useAppSelector(state => state.auth)
 
+  const isSimpleAuth = store.authType === 'simple' && store.anthEnable
+
+  const schema = useMemo(() => createDefaultLoginSchema({ isSimpleAuth }), 
[isSimpleAuth])
+
   const {
     control,
     handleSubmit,
     reset,
     formState: { errors }
   } = useForm({
-    defaultValues: Object.assign({}, defaultValues),
+    defaultValues: Object.assign({}, DEFAULT_LOGIN_DEFAULT_VALUES),
     mode: 'onChange',
     resolver: yupResolver(schema)
   })
@@ -66,8 +56,13 @@ function DefaultLogin() {
   }, [store.intervalId])
 
   const onSubmit = async data => {
-    await dispatch(loginAction({ params: data, router }))
-    await dispatch(setIntervalIdAction())
+    if (isSimpleAuth) {
+      await dispatch(setAuthUser({ name: String(data.username || '').trim(), 
type: 'user' }))
+      router.push('/metalakes')
+    } else {
+      await dispatch(loginAction({ params: data, router }))
+      await dispatch(setIntervalIdAction())
+    }
 
     reset({ ...data })
   }
@@ -78,86 +73,120 @@ function DefaultLogin() {
 
   return (
     <form autoComplete='off' onSubmit={handleSubmit(onSubmit, onError)}>
-      <Grid item xs={12} sx={{ mt: 4 }}>
-        <FormControl fullWidth>
-          <Controller
-            name='grant_type'
-            control={control}
-            rules={{ required: true }}
-            render={({ field: { value, onChange } }) => (
-              <TextField
-                value={value}
-                label='Grant Type'
-                disabled
-                onChange={onChange}
-                placeholder=''
-                error={Boolean(errors.grant_type)}
-              />
+      {isSimpleAuth ? (
+        <Grid item xs={12} sx={{ mt: 4 }}>
+          <FormControl fullWidth>
+            <Controller
+              name='username'
+              control={control}
+              rules={{ required: true }}
+              render={({ field: { value, onChange } }) => (
+                <TextField
+                  value={value}
+                  label='Username'
+                  onChange={onChange}
+                  placeholder=''
+                  error={Boolean(errors.username)}
+                />
+              )}
+            />
+            {errors.username && (
+              <FormHelperText 
className={'twc-text-error-main'}>{errors.username.message}</FormHelperText>
             )}
-          />
-          {errors.grant_type && (
-            <FormHelperText 
className={'twc-text-error-main'}>{errors.grant_type.message}</FormHelperText>
-          )}
-        </FormControl>
-      </Grid>
-
-      <Grid item xs={12} sx={{ mt: 4 }}>
-        <FormControl fullWidth>
-          <Controller
-            name='client_id'
-            control={control}
-            rules={{ required: true }}
-            render={({ field: { value, onChange } }) => (
-              <TextField
-                value={value}
-                label='Client ID'
-                onChange={onChange}
-                placeholder=''
-                error={Boolean(errors.client_id)}
+          </FormControl>
+        </Grid>
+      ) : (
+        <>
+          <Grid item xs={12} sx={{ mt: 4 }}>
+            <FormControl fullWidth>
+              <Controller
+                name='grant_type'
+                control={control}
+                rules={{ required: true }}
+                render={({ field: { value, onChange } }) => (
+                  <TextField
+                    value={value}
+                    label='Grant Type'
+                    disabled
+                    onChange={onChange}
+                    placeholder=''
+                    error={Boolean(errors.grant_type)}
+                  />
+                )}
               />
-            )}
-          />
-          {errors.client_id && (
-            <FormHelperText 
className={'twc-text-error-main'}>{errors.client_id.message}</FormHelperText>
-          )}
-        </FormControl>
-      </Grid>
-
-      <Grid item xs={12} sx={{ mt: 4 }}>
-        <FormControl fullWidth>
-          <Controller
-            name='client_secret'
-            control={control}
-            rules={{ required: true }}
-            render={({ field: { value, onChange } }) => (
-              <TextField
-                value={value}
-                label='Client Secret'
-                onChange={onChange}
-                placeholder=''
-                error={Boolean(errors.client_secret)}
+              {errors.grant_type && (
+                <FormHelperText 
className={'twc-text-error-main'}>{errors.grant_type.message}</FormHelperText>
+              )}
+            </FormControl>
+          </Grid>
+
+          <Grid item xs={12} sx={{ mt: 4 }}>
+            <FormControl fullWidth>
+              <Controller
+                name='client_id'
+                control={control}
+                rules={{ required: true }}
+                render={({ field: { value, onChange } }) => (
+                  <TextField
+                    value={value}
+                    label='Client ID'
+                    onChange={onChange}
+                    placeholder=''
+                    error={Boolean(errors.client_id)}
+                  />
+                )}
               />
-            )}
-          />
-          {errors.client_secret && (
-            <FormHelperText 
className={'twc-text-error-main'}>{errors.client_secret.message}</FormHelperText>
-          )}
-        </FormControl>
-      </Grid>
-
-      <Grid item xs={12} sx={{ mt: 4 }}>
-        <FormControl fullWidth>
-          <Controller
-            name='scope'
-            control={control}
-            rules={{ required: true }}
-            render={({ field: { value, onChange } }) => (
-              <TextField value={value} label='Scope' onChange={onChange} 
placeholder='' error={Boolean(errors.scope)} />
-            )}
-          />
-          {errors.scope && <FormHelperText 
className={'twc-text-error-main'}>{errors.scope.message}</FormHelperText>}
-        </FormControl>
-      </Grid>
+              {errors.client_id && (
+                <FormHelperText 
className={'twc-text-error-main'}>{errors.client_id.message}</FormHelperText>
+              )}
+            </FormControl>
+          </Grid>
+
+          <Grid item xs={12} sx={{ mt: 4 }}>
+            <FormControl fullWidth>
+              <Controller
+                name='client_secret'
+                control={control}
+                rules={{ required: true }}
+                render={({ field: { value, onChange } }) => (
+                  <TextField
+                    value={value}
+                    label='Client Secret'
+                    onChange={onChange}
+                    placeholder=''
+                    error={Boolean(errors.client_secret)}
+                  />
+                )}
+              />
+              {errors.client_secret && (
+                <FormHelperText 
className={'twc-text-error-main'}>{errors.client_secret.message}</FormHelperText>
+              )}
+            </FormControl>
+          </Grid>
+
+          <Grid item xs={12} sx={{ mt: 4 }}>
+            <FormControl fullWidth>
+              <Controller
+                name='scope'
+                control={control}
+                rules={{ required: true }}
+                render={({ field: { value, onChange } }) => (
+                  <TextField
+                    value={value}
+                    label='Scope'
+                    onChange={onChange}
+                    placeholder=''
+                    error={Boolean(errors.scope)}
+                  />
+                )}
+              />
+              {errors.scope && (
+                <FormHelperText 
className={'twc-text-error-main'}>{errors.scope.message}</FormHelperText>
+              )}
+            </FormControl>
+          </Grid>
+        </>
+      )}
 
       <Button fullWidth size='large' type='submit' variant='contained' sx={{ 
mb: 7, mt: 12 }}>
         Login
diff --git a/web/web/src/app/login/components/defaultLoginSchema.js 
b/web/web/src/app/login/components/defaultLoginSchema.js
new file mode 100644
index 0000000000..43874d831f
--- /dev/null
+++ b/web/web/src/app/login/components/defaultLoginSchema.js
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as yup from 'yup'
+
+export const DEFAULT_LOGIN_DEFAULT_VALUES = {
+  grant_type: 'client_credentials',
+  client_id: '',
+  client_secret: '',
+  scope: '',
+  username: ''
+}
+
+export function createDefaultLoginSchema({ isSimpleAuth }) {
+  if (isSimpleAuth) {
+    return yup.object().shape({
+      username: yup.string().trim().required('Username is required'),
+      grant_type: yup.string().notRequired(),
+      client_id: yup.string().notRequired(),
+      client_secret: yup.string().notRequired(),
+      scope: yup.string().notRequired()
+    })
+  }
+
+  return yup.object().shape({
+    grant_type: yup.string().required(),
+    client_id: yup.string().required(),
+    client_secret: yup.string().required(),
+    scope: yup.string().required(),
+    username: yup.string().notRequired()
+  })
+}
diff --git a/web/web/src/app/rootLayout/Logout.js 
b/web/web/src/app/rootLayout/Logout.js
index 1cca5ee49b..81803be302 100644
--- a/web/web/src/app/rootLayout/Logout.js
+++ b/web/web/src/app/rootLayout/Logout.js
@@ -22,7 +22,7 @@
 import { useRouter } from 'next/navigation'
 import { useState, useEffect } from 'react'
 
-import { Box, IconButton } from '@mui/material'
+import { Box, IconButton, Tooltip } from '@mui/material'
 
 import Icon from '@/components/Icon'
 import { useAppDispatch, useAppSelector } from '@/lib/hooks/useStore'
@@ -34,6 +34,8 @@ const LogoutButton = () => {
   const dispatch = useAppDispatch()
   const authStore = useAppSelector(state => state.auth)
   const [showLogoutButton, setShowLogoutButton] = useState(false)
+  const authEnabled = authStore.anthEnable === true || authStore.anthEnable 
=== 'true'
+  const userName = authStore.authUser?.name
 
   useEffect(() => {
     const checkAuthStatus = async () => {
@@ -68,10 +70,12 @@ const LogoutButton = () => {
 
   return (
     <Box>
-      {showLogoutButton ? (
-        <IconButton onClick={handleLogout}>
-          <Icon icon={'bx:exit'} />
-        </IconButton>
+      {(authEnabled && userName) || showLogoutButton ? (
+        <Tooltip title={userName || ''} disableHoverListener={!userName}>
+          <IconButton onClick={handleLogout}>
+            <Icon icon={'bx:exit'} />
+          </IconButton>
+        </Tooltip>
       ) : null}
     </Box>
   )
diff --git a/web/web/src/lib/auth/providers/generic.js 
b/web/web/src/lib/auth/providers/generic.js
index b235e94244..54f617e32c 100644
--- a/web/web/src/lib/auth/providers/generic.js
+++ b/web/web/src/lib/auth/providers/generic.js
@@ -53,6 +53,14 @@ export class GenericOAuthProvider extends BaseOAuthProvider {
     return !!token
   }
 
+  /**
+   * Get current user profile
+   */
+  async getUserProfile() {
+    // Generic OAuth provider doesn't have user profile, default to a test user
+    return this.getAccessToken() ? { name: 'test' } : null
+  }
+
   /**
    * Clear authentication data
    */
diff --git a/web/web/src/lib/provider/session.js 
b/web/web/src/lib/provider/session.js
index 62569022f9..4ab33bfb6a 100644
--- a/web/web/src/lib/provider/session.js
+++ b/web/web/src/lib/provider/session.js
@@ -29,7 +29,7 @@ import { initialVersion, fetchGitHubInfo } from 
'@/lib/store/sys'
 import { oauthProviderFactory } from '@/lib/auth/providers/factory'
 
 import { to } from '../utils'
-import { getAuthConfigs, setAuthToken } from '../store/auth'
+import { getAuthConfigs, setAuthToken, setAuthUser } from '../store/auth'
 
 import { useIdle } from 'react-use'
 
@@ -86,14 +86,28 @@ const AuthProvider = ({ children }) => {
     const initAuth = async () => {
       const [authConfigsErr, resAuthConfigs] = await 
to(dispatch(getAuthConfigs()))
       const authType = resAuthConfigs?.payload?.authType
+      const anthEnable = resAuthConfigs?.payload?.anthEnable
 
       // Always fetch GitHub info since it's a public API call
       dispatch(fetchGitHubInfo())
 
       if (authType === 'simple') {
         dispatch(initialVersion())
-        goToMetalakeListPage()
+        const sessionUser = typeof window !== 'undefined' && 
JSON.parse(window.sessionStorage.getItem('simpleAuthUser'))
+        if (anthEnable && !sessionUser) {
+          router.push('/login')
+        } else {
+          dispatch(setAuthUser(sessionUser))
+          goToMetalakeListPage()
+        }
       } else if (authType === 'oauth') {
+        let provider = null
+        try {
+          provider = await oauthProviderFactory.getProvider()
+        } catch (e) {
+          provider = null
+        }
+
         const tokenToUse = await oauthProviderFactory.getAccessToken()
 
         // Update local token state
@@ -101,6 +115,21 @@ const AuthProvider = ({ children }) => {
 
         if (tokenToUse) {
           dispatch(setAuthToken(tokenToUse))
+
+          // Best-effort: hydrate auth user from OAuth provider profile.
+          // Do not block navigation if the profile cannot be loaded.
+          try {
+            const profile = provider?.getUserProfile ? await 
provider.getUserProfile() : null
+
+            const displayName =
+              profile?.preferred_username || profile?.name || profile?.email 
|| profile?.sub || profile?.id
+            if (displayName) {
+              dispatch(setAuthUser({ name: String(displayName), type: 'user', 
profile }))
+            }
+          } catch (e) {
+            // Ignore profile errors
+          }
+
           dispatch(initialVersion())
           goToMetalakeListPage()
         } else {
diff --git a/web/web/src/lib/store/auth/index.js 
b/web/web/src/lib/store/auth/index.js
index cce630e5e7..0e5e3739bc 100644
--- a/web/web/src/lib/store/auth/index.js
+++ b/web/web/src/lib/store/auth/index.js
@@ -32,6 +32,7 @@ const devOauthUrl = process.env.NEXT_PUBLIC_OAUTH_PATH
 export const getAuthConfigs = createAsyncThunk('auth/getAuthConfigs', async () 
=> {
   let oauthUrl = null
   let authType = null
+  let anthEnable = null
   const [err, res] = await to(getAuthConfigsApi())
 
   if (err || !res) {
@@ -42,10 +43,11 @@ export const getAuthConfigs = 
createAsyncThunk('auth/getAuthConfigs', async () =
 
   // ** get the first authenticator from the response. response example: 
"[simple, oauth]"
   authType = res['gravitino.authenticators'][0].trim()
+  anthEnable = res['gravitino.authorization.enable']
 
   localStorage.setItem('oauthUrl', oauthUrl)
 
-  return { oauthUrl, authType }
+  return { oauthUrl, authType, anthEnable }
 })
 
 export const refreshToken = createAsyncThunk('auth/refreshToken', async (data, 
{ getState, dispatch }) => {
@@ -93,25 +95,29 @@ export const loginAction = 
createAsyncThunk('auth/loginAction', async ({ params,
 
 export const logoutAction = createAsyncThunk('auth/logoutAction', async ({ 
router }, { getState, dispatch }) => {
   // Clear provider authentication data first
-  try {
-    const provider = await oauthProviderFactory.getProvider()
-    if (provider) {
-      await provider.clearAuthData()
-      console.log('[Logout Action] Provider cleanup completed')
+  if (getState().auth.authType === 'oauth') {
+    try {
+      const provider = await oauthProviderFactory.getProvider()
+      if (provider) {
+        await provider.clearAuthData()
+        console.log('[Logout Action] Provider cleanup completed')
+      }
+    } catch (error) {
+      console.warn('[Logout Action] Provider cleanup failed:', error)
     }
-  } catch (error) {
-    console.warn('[Logout Action] Provider cleanup failed:', error)
-  }
-
-  // Clear legacy auth tokens
-  localStorage.removeItem('accessToken')
-  localStorage.removeItem('authParams')
-  localStorage.removeItem('expiredIn')
-  localStorage.removeItem('isIdle')
-  localStorage.removeItem('version')
 
-  dispatch(clearIntervalId())
-  dispatch(setAuthToken(''))
+    // Clear legacy auth tokens
+    localStorage.removeItem('accessToken')
+    localStorage.removeItem('authParams')
+    localStorage.removeItem('expiredIn')
+    localStorage.removeItem('isIdle')
+    localStorage.removeItem('version')
+
+    dispatch(clearIntervalId())
+    dispatch(setAuthToken(''))
+  } else {
+    dispatch(setAuthUser(null))
+  }
   await router.push('/login')
 
   return { token: null }
@@ -149,7 +155,9 @@ export const authSlice = createSlice({
     authToken: null,
     authParams: null,
     expiredIn: null,
-    intervalId: null
+    intervalId: null,
+    anthEnable: null,
+    authUser: null
   },
   reducers: {
     setIntervalId(state, action) {
@@ -169,12 +177,21 @@ export const authSlice = createSlice({
     },
     setExpiredIn(state, action) {
       state.expiredIn = action.payload
+    },
+    setAuthUser(state, action) {
+      if (action.payload) {
+        sessionStorage.setItem('simpleAuthUser', 
JSON.stringify(action.payload))
+      } else {
+        sessionStorage.removeItem('simpleAuthUser')
+      }
+      state.authUser = action.payload
     }
   },
   extraReducers: builder => {
     builder.addCase(getAuthConfigs.fulfilled, (state, action) => {
       state.oauthUrl = action.payload.oauthUrl
       state.authType = action.payload.authType
+      state.anthEnable = action.payload.anthEnable
     })
     builder.addCase(refreshToken.fulfilled, (state, action) => {
       localStorage.setItem('accessToken', action.payload.token)
@@ -186,6 +203,6 @@ export const authSlice = createSlice({
   }
 })
 
-export const { setAuthToken, setAuthParams, setExpiredIn, clearIntervalId } = 
authSlice.actions
+export const { setAuthToken, setAuthParams, setExpiredIn, clearIntervalId, 
setAuthUser } = authSlice.actions
 
 export default authSlice.reducer
diff --git a/web/web/src/lib/utils/axios/index.js 
b/web/web/src/lib/utils/axios/index.js
index a0710bb08b..d9df97f88d 100644
--- a/web/web/src/lib/utils/axios/index.js
+++ b/web/web/src/lib/utils/axios/index.js
@@ -187,6 +187,13 @@ const transform = {
       if (token && config?.requestOptions?.withToken !== false) {
         // ** jwt token
         config.headers.Authorization = options.authenticationScheme ? 
`${options.authenticationScheme} ${token}` : token
+      } else if (window.sessionStorage.getItem('simpleAuthUser')) {
+        // Simple auth fallback
+        const simpleAuthToken = 
window.sessionStorage.getItem('simpleAuthToken')
+        const user = 
JSON.parse(window.sessionStorage.getItem('simpleAuthUser'))?.name
+        if (user) {
+          config.headers.Authorization = `Basic ${Buffer.from(user || 
'').toString('base64')}`
+        }
       }
     } catch (error) {
       console.warn('Failed to get access token:', error)

Reply via email to