This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 53fcaa993d [#10535] fix(server,web-v2): Expose serviceAdmins in
/configs and handle array format in UI (#10523)
53fcaa993d is described below
commit 53fcaa993d2de78407142e7d5363fc179263499c
Author: Bharath Krishna <[email protected]>
AuthorDate: Wed Mar 25 04:38:49 2026 -0700
[#10535] fix(server,web-v2): Expose serviceAdmins in /configs and handle
array format in UI (#10523)
### What changes were proposed in this pull request?
- Add Configs.SERVICE_ADMINS to ConfigServlet basicConfigEntries so
gravitino.authorization.serviceAdmins is returned by GET /configs
- Update TestConfigServlet to assert the new field is present
- Fix web-v2 UI to handle serviceAdmins as JSON array (List<String>)
rather than a comma-separated string
It is accessed in front-end here :
https://github.com/apache/gravitino/blob/main/web-v2/web/src/lib/store/auth/index.js#L48
### Why are the changes needed?
ConfigServlet only exposed AUTHENTICATORS and ENABLE_AUTHORIZATION via
the /configs endpoint. Without serviceAdmins, the web UI cannot
determine if the logged-in user is a service admin, causing the 'Create
Metalake' button to be hidden even for valid service admins.
Fix: #10535
### Does this PR introduce _any_ user-facing change?
Yes, it will correctly make the "CREATE METALAKE" button appear
### How was this patch tested?
Testing on Web UI
<img width="468" height="233" alt="Screenshot 2026-03-23 at 9 57 07 PM"
src="https://github.com/user-attachments/assets/7ed96155-7812-4b32-bed6-5409f621bd16"
/>
---
.../apache/gravitino/server/web/ConfigServlet.java | 9 ++++++
.../gravitino/server/web/TestConfigServlet.java | 35 ++++++++++++++++++++++
web-v2/web/src/app/metalakes/page.js | 6 ++--
web-v2/web/src/app/rootLayout/UserSetting.js | 4 ++-
4 files changed, 51 insertions(+), 3 deletions(-)
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/ConfigServlet.java
b/server/src/main/java/org/apache/gravitino/server/web/ConfigServlet.java
index a1f8d84698..3b48cc4295 100644
--- a/server/src/main/java/org/apache/gravitino/server/web/ConfigServlet.java
+++ b/server/src/main/java/org/apache/gravitino/server/web/ConfigServlet.java
@@ -58,6 +58,15 @@ public class ConfigServlet extends HttpServlet {
configs.put(key.getKey(), serverConfig.get(key));
}
+ if (serverConfig.get(Configs.ENABLE_AUTHORIZATION)) {
+ // Expose serviceAdmins when authorization is enabled so the web UI can
determine whether the
+ // logged-in user has service-admin privileges (e.g. to show the "Create
Metalake" button).
+ String serviceAdminsRaw =
serverConfig.getRawString(Configs.SERVICE_ADMINS.getKey());
+ if (serviceAdminsRaw != null) {
+ configs.put(Configs.SERVICE_ADMINS.getKey(),
serverConfig.get(Configs.SERVICE_ADMINS));
+ }
+ }
+
if (serverConfig
.get(Configs.AUTHENTICATORS)
.contains(AuthenticatorType.OAUTH.name().toLowerCase())) {
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/TestConfigServlet.java
b/server/src/test/java/org/apache/gravitino/server/web/TestConfigServlet.java
index aab753fac2..d165dc2b42 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/TestConfigServlet.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/TestConfigServlet.java
@@ -52,6 +52,41 @@ public class TestConfigServlet {
configServlet.destroy();
}
+ @Test
+ public void testConfigServletWithAuthEnabledButNoServiceAdmins() throws
Exception {
+ // When authorization is enabled but serviceAdmins is not configured, the
key should be
+ // absent from the response (no crash) rather than null or empty.
+ ServerConfig serverConfig = new ServerConfig();
+ serverConfig.set(Configs.ENABLE_AUTHORIZATION, true);
+ ConfigServlet configServlet = new ConfigServlet(serverConfig);
+ configServlet.init();
+ HttpServletResponse res = mock(HttpServletResponse.class);
+ PrintWriter writer = mock(PrintWriter.class);
+ when(res.getWriter()).thenReturn(writer);
+ configServlet.doGet(null, res);
+ verify(writer)
+ .write(
+
"{\"gravitino.authorization.enable\":true,\"gravitino.authenticators\":[\"simple\"]}");
+ configServlet.destroy();
+ }
+
+ @Test
+ public void testConfigServletWithAuthEnabledAndServiceAdmins() throws
Exception {
+ ServerConfig serverConfig = new ServerConfig();
+ serverConfig.set(Configs.ENABLE_AUTHORIZATION, true);
+ serverConfig.set(Configs.SERVICE_ADMINS, Lists.newArrayList("admin1",
"admin2"));
+ ConfigServlet configServlet = new ConfigServlet(serverConfig);
+ configServlet.init();
+ HttpServletResponse res = mock(HttpServletResponse.class);
+ PrintWriter writer = mock(PrintWriter.class);
+ when(res.getWriter()).thenReturn(writer);
+ configServlet.doGet(null, res);
+ verify(writer)
+ .write(
+
"{\"gravitino.authorization.enable\":true,\"gravitino.authenticators\":[\"simple\"],\"gravitino.authorization.serviceAdmins\":[\"admin1\",\"admin2\"]}");
+ configServlet.destroy();
+ }
+
@Test
public void testConfigServletWithVisibleConfigs() throws Exception {
ServerConfig serverConfig = new ServerConfig();
diff --git a/web-v2/web/src/app/metalakes/page.js
b/web-v2/web/src/app/metalakes/page.js
index 8b8fcb17b5..183b04a329 100644
--- a/web-v2/web/src/app/metalakes/page.js
+++ b/web-v2/web/src/app/metalakes/page.js
@@ -70,6 +70,8 @@ const MetalakeList = () => {
const [ownerRefreshKey, setOwnerRefreshKey] = useState(0)
const auth = useAppSelector(state => state.auth)
const { serviceAdmins, authUser, anthEnable } = auth
+ const admins = Array.isArray(serviceAdmins) ? serviceAdmins : (serviceAdmins
|| '').split(',')
+ const isServiceAdmin = admins.includes(authUser?.name)
const dispatch = useAppDispatch()
const store = useAppSelector(state => state.metalakes)
const [tableData, setTableData] = useState([])
@@ -273,7 +275,7 @@ const MetalakeList = () => {
return (
<div className='flex gap-2' key={record.name}>
<NameContext.Provider
value={record.name}>{contextHolder}</NameContext.Provider>
- {([...(serviceAdmins || '').split(',')].includes(authUser?.name)
|| !authUser) && (
+ {(isServiceAdmin || !authUser) && (
<a data-refer={`edit-metalake-${record.name}`}>
<Tooltip title='Edit'>
<Icons.Pencil className='size-4' onClick={() =>
handleEditMetalake(record.name)} />
@@ -361,7 +363,7 @@ const MetalakeList = () => {
placeholder='Search...'
onChange={onSearchTable}
/>
- {([...(serviceAdmins || '').split(',')].includes(authUser?.name)
|| !anthEnable) && (
+ {(isServiceAdmin || !anthEnable) && (
<Button
data-refer='create-metalake-btn'
type='primary'
diff --git a/web-v2/web/src/app/rootLayout/UserSetting.js
b/web-v2/web/src/app/rootLayout/UserSetting.js
index dbbe97e962..d3041ae2a8 100644
--- a/web-v2/web/src/app/rootLayout/UserSetting.js
+++ b/web-v2/web/src/app/rootLayout/UserSetting.js
@@ -41,6 +41,8 @@ export default function UserSetting() {
const [showLogoutButton, setShowLogoutButton] = useState(false)
const auth = useAppSelector(state => state.auth)
const { serviceAdmins, authUser, anthEnable } = auth
+ const admins = Array.isArray(serviceAdmins) ? serviceAdmins : (serviceAdmins
|| '').split(',')
+ const isServiceAdmin = admins.includes(authUser?.name)
const [session, setSession] = useState({})
const router = useRouter()
const pathname = usePathname()
@@ -85,7 +87,7 @@ export default function UserSetting() {
label: (
<div className='flex w-[208px] justify-between'>
<span>Metalakes</span>
- {[...(serviceAdmins || '').split(',')].includes(authUser?.name) &&
(
+ {isServiceAdmin && (
<Tooltip title='Create Metalake'>
<PlusOutlined className='cursor-pointer text-black'
onClick={handleCreateMetalake} />
</Tooltip>