diff --git a/amoro-ams/pom.xml b/amoro-ams/pom.xml
index 2fe19d0aa9..c4ce50f3dd 100644
--- a/amoro-ams/pom.xml
+++ b/amoro-ams/pom.xml
@@ -436,7 +436,10 @@
1.19.6
test
-
+
+ org.casbin
+ jcasbin
+
diff --git a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java
index 283d211dd8..d6fb8a0202 100644
--- a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java
@@ -38,6 +38,8 @@
import org.apache.amoro.server.dashboard.utils.CommonUtil;
import org.apache.amoro.server.manager.EventsManager;
import org.apache.amoro.server.manager.MetricManager;
+import org.apache.amoro.server.permission.PermissionManager;
+import org.apache.amoro.server.permission.UserInfoManager;
import org.apache.amoro.server.persistence.DataSourceFactory;
import org.apache.amoro.server.persistence.HttpSessionHandlerFactory;
import org.apache.amoro.server.persistence.SqlSessionFactoryProvider;
@@ -109,6 +111,8 @@ public class AmoroServiceContainer {
private TServer optimizingServiceServer;
private Javalin httpServer;
private AmsServiceMetrics amsServiceMetrics;
+ private UserInfoManager userInfoManager;
+ private PermissionManager permissionManager;
public AmoroServiceContainer() throws Exception {
initConfig();
@@ -163,7 +167,8 @@ public void startService() throws Exception {
optimizingService =
new DefaultOptimizingService(serviceConfig, catalogManager, optimizerManager, tableService);
-
+ userInfoManager = new UserInfoManager();
+ permissionManager = new PermissionManager();
LOG.info("Setting up AMS table executors...");
AsyncTableExecutors.getInstance().setup(tableService, serviceConfig);
addHandlerChain(optimizingService.getTableRuntimeHandler());
@@ -262,7 +267,9 @@ private void initHttpService() {
tableManager,
optimizerManager,
optimizingService,
- terminalManager);
+ terminalManager,
+ userInfoManager,
+ permissionManager);
RestCatalogService restCatalogService = new RestCatalogService(catalogManager, tableManager);
httpServer =
diff --git a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java
index b5e71bc162..d3d972d5ac 100644
--- a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java
@@ -32,6 +32,7 @@
import io.javalin.http.staticfiles.Location;
import io.javalin.http.staticfiles.StaticFileConfig;
import org.apache.amoro.config.Configurations;
+import org.apache.amoro.exception.AccessDeniedException;
import org.apache.amoro.exception.ForbiddenException;
import org.apache.amoro.exception.SignatureCheckException;
import org.apache.amoro.server.AmoroManagementConf;
@@ -49,8 +50,11 @@
import org.apache.amoro.server.dashboard.controller.TableController;
import org.apache.amoro.server.dashboard.controller.TerminalController;
import org.apache.amoro.server.dashboard.controller.VersionController;
+import org.apache.amoro.server.dashboard.model.SessionInfo;
import org.apache.amoro.server.dashboard.response.ErrorResponse;
import org.apache.amoro.server.dashboard.utils.ParamSignatureCalculator;
+import org.apache.amoro.server.permission.PermissionManager;
+import org.apache.amoro.server.permission.UserInfoManager;
import org.apache.amoro.server.resource.OptimizerManager;
import org.apache.amoro.server.table.TableManager;
import org.apache.amoro.server.terminal.TerminalManager;
@@ -93,6 +97,8 @@ public class DashboardServer {
private final String authType;
private final String basicAuthUser;
private final String basicAuthPassword;
+ private final UserInfoManager userInfoManager;
+ private final PermissionManager permissionManager;
public DashboardServer(
Configurations serviceConfig,
@@ -100,11 +106,13 @@ public DashboardServer(
TableManager tableManager,
OptimizerManager optimizerManager,
DefaultOptimizingService optimizingService,
- TerminalManager terminalManager) {
+ TerminalManager terminalManager,
+ UserInfoManager userInfoManager,
+ PermissionManager permissionManager) {
PlatformFileManager platformFileManager = new PlatformFileManager();
this.catalogController = new CatalogController(catalogManager, platformFileManager);
this.healthCheckController = new HealthCheckController();
- this.loginController = new LoginController(serviceConfig);
+ this.loginController = new LoginController(serviceConfig, userInfoManager);
// TODO: remove table service from OptimizerGroupController
this.optimizerGroupController =
new OptimizerGroupController(tableManager, optimizingService, optimizerManager);
@@ -124,6 +132,8 @@ public DashboardServer(
this.authType = serviceConfig.get(AmoroManagementConf.HTTP_SERVER_REST_AUTH_TYPE);
this.basicAuthUser = serviceConfig.get(AmoroManagementConf.ADMIN_USERNAME);
this.basicAuthPassword = serviceConfig.get(AmoroManagementConf.ADMIN_PASSWORD);
+ this.userInfoManager = userInfoManager;
+ this.permissionManager = permissionManager;
}
private volatile String indexHtml = null;
@@ -387,15 +397,26 @@ public void preHandleRequest(Context ctx) {
if (null == ctx.sessionAttribute("user")) {
throw new ForbiddenException("User session attribute is missed for url: " + uriPath);
}
+ // TODO : check permission
+ SessionInfo user = ctx.sessionAttribute("user");
+ String method = ctx.method();
+ String path = ctx.path();
+ if (!permissionManager.accessible(user.getUserName(), path, method)) {
+ throw new AccessDeniedException("unable to access url: " + uriPath);
+ }
return;
}
if (AUTH_TYPE_BASIC.equalsIgnoreCase(authType)) {
BasicAuthCredentials cred = ctx.basicAuthCredentials();
- if (!(basicAuthUser.equals(cred.component1())
- && basicAuthPassword.equals(cred.component2()))) {
+ if (!userInfoManager.isValidate(cred.component1(), cred.component2())) {
throw new SignatureCheckException(
"Failed to authenticate via basic authentication for url:" + uriPath);
}
+ // if (!(basicAuthUser.equals(cred.component1())
+ // && basicAuthPassword.equals(cred.component2()))) {
+ // throw new SignatureCheckException(
+ // "Failed to authenticate via basic authentication for url:" + uriPath);
+ // }
} else {
checkApiToken(
ctx.url(), ctx.queryParam("apiKey"), ctx.queryParam("signature"), ctx.queryParamMap());
@@ -412,6 +433,14 @@ public void handleException(Exception e, Context ctx) {
}
} else if (e instanceof SignatureCheckException) {
ctx.json(new ErrorResponse(HttpCode.FORBIDDEN, "Signature check failed", ""));
+ } else if (e instanceof AccessDeniedException) {
+ if (!ctx.req.getRequestURI().startsWith("/api/ams")) {
+ ctx.html(getIndexFileContent());
+ } else {
+ ctx.status(HttpCode.FORBIDDEN);
+ ctx.json(new ErrorResponse(HttpCode.FORBIDDEN, "Access Denied", ""));
+ return;
+ }
} else {
ctx.json(new ErrorResponse(HttpCode.INTERNAL_SERVER_ERROR, e.getMessage(), ""));
}
diff --git a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java
index c9b61fee7c..cd5a26c1d8 100644
--- a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java
@@ -21,9 +21,10 @@
import io.javalin.http.Context;
import org.apache.amoro.config.Configurations;
import org.apache.amoro.server.AmoroManagementConf;
+import org.apache.amoro.server.dashboard.model.SessionInfo;
import org.apache.amoro.server.dashboard.response.OkResponse;
+import org.apache.amoro.server.permission.UserInfoManager;
-import java.io.Serializable;
import java.util.Map;
/** The controller that handles login requests. */
@@ -31,10 +32,12 @@ public class LoginController {
private final String adminUser;
private final String adminPassword;
+ private final UserInfoManager userInfoManager;
- public LoginController(Configurations serviceConfig) {
+ public LoginController(Configurations serviceConfig, UserInfoManager userInfoManager) {
adminUser = serviceConfig.get(AmoroManagementConf.ADMIN_USERNAME);
adminPassword = serviceConfig.get(AmoroManagementConf.ADMIN_PASSWORD);
+ this.userInfoManager = userInfoManager;
}
/** Get current user. */
@@ -49,8 +52,8 @@ public void login(Context ctx) {
Map bodyParams = ctx.bodyAsClass(Map.class);
String user = bodyParams.get("user");
String pwd = bodyParams.get("password");
- if (adminUser.equals(user) && (adminPassword.equals(pwd))) {
- ctx.sessionAttribute("user", new SessionInfo(adminUser, System.currentTimeMillis() + ""));
+ if (userInfoManager.isValidate(user, pwd)) {
+ ctx.sessionAttribute("user", new SessionInfo(user, System.currentTimeMillis() + ""));
ctx.json(OkResponse.of("success"));
} else {
throw new RuntimeException("bad user " + user + " or password!");
@@ -62,30 +65,4 @@ public void logout(Context ctx) {
ctx.removeCookie("JSESSIONID");
ctx.json(OkResponse.ok());
}
-
- static class SessionInfo implements Serializable {
- String userName;
- String loginTime;
-
- public SessionInfo(String username, String loginTime) {
- this.userName = username;
- this.loginTime = loginTime;
- }
-
- public String getUserName() {
- return userName;
- }
-
- public void setUserName(String userName) {
- this.userName = userName;
- }
-
- public String getLoginTime() {
- return loginTime;
- }
-
- public void setLoginTime(String loginTime) {
- this.loginTime = loginTime;
- }
- }
}
diff --git a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/model/SessionInfo.java b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/model/SessionInfo.java
index 9d0abb3be1..1b5efdfb74 100644
--- a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/model/SessionInfo.java
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/model/SessionInfo.java
@@ -20,11 +20,35 @@
public class SessionInfo {
private String sessionId;
+ String userName;
+
+ public String getLoginTime() {
+ return loginTime;
+ }
+
+ public void setLoginTime(String loginTime) {
+ this.loginTime = loginTime;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ String loginTime;
public SessionInfo(String sessionId) {
this.sessionId = sessionId;
}
+ public SessionInfo(String userName, String loginTime) {
+ this.userName = userName;
+ this.loginTime = loginTime;
+ }
+
public SessionInfo() {}
public String getSessionId() {
diff --git a/amoro-ams/src/main/java/org/apache/amoro/server/permission/PermissionManager.java b/amoro-ams/src/main/java/org/apache/amoro/server/permission/PermissionManager.java
new file mode 100644
index 0000000000..d1a000bac8
--- /dev/null
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/permission/PermissionManager.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+package org.apache.amoro.server.permission;
+
+import org.apache.amoro.server.Environments;
+import org.casbin.jcasbin.main.Enforcer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+public class PermissionManager {
+
+ public static final Logger LOG = LoggerFactory.getLogger(UserInfoManager.class);
+
+ private final Enforcer enforcer;
+
+ public PermissionManager() {
+ String modelPath = Environments.getConfigPath() + "/rbac_model.conf";
+ String policyPath = Environments.getConfigPath() + "/policy.csv";
+ File modelFile = new File(modelPath);
+ File policyFile = new File(policyPath);
+ if (!modelFile.exists() || !policyFile.exists()) {
+ enforcer = new Enforcer();
+ LOG.warn("model or policy file not exist, please check your config");
+ return;
+ }
+ enforcer = new Enforcer(modelPath, policyPath);
+ }
+
+ public boolean accessible(String user, String url, String method) {
+ if (!enforcer.enforce(user, url, method)) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/amoro-ams/src/main/java/org/apache/amoro/server/permission/UserInfoManager.java b/amoro-ams/src/main/java/org/apache/amoro/server/permission/UserInfoManager.java
new file mode 100644
index 0000000000..3b14ea285c
--- /dev/null
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/permission/UserInfoManager.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+package org.apache.amoro.server.permission;
+
+import org.apache.amoro.server.Environments;
+import org.apache.amoro.shade.guava32.com.google.common.collect.Maps;
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.Map;
+
+public class UserInfoManager {
+
+ public static final Logger LOG = LoggerFactory.getLogger(UserInfoManager.class);
+
+ private final Map users = Maps.newHashMap();
+
+ public UserInfoManager() {
+ String configPath = Environments.getConfigPath() + "/users.csv";
+ this.loadUserInfoFileToMap(configPath);
+ }
+
+ public boolean isValidate(String username, String password) {
+ if (users.containsKey(username)) {
+ return users.get(username).equals(password);
+ }
+ return false;
+ }
+
+ private void loadUserInfoFileToMap(String filePath) {
+ try {
+ File file = new File(filePath);
+ if (!file.exists()) {
+ LOG.warn("userInfo file not exist, please check your config");
+ return;
+ }
+ FileUtils.readLines(file, "UTF-8")
+ .forEach(
+ line -> {
+ String[] parts = line.split(",");
+ if (parts.length == 2) {
+ String username = parts[0].trim();
+ String password = parts[1].trim();
+ users.put(username, password);
+ }
+ });
+ } catch (Exception e) {
+ LOG.error("load userInfo file error", e);
+ throw new RuntimeException("load userInfo file error", e);
+ }
+ }
+}
diff --git a/amoro-common/src/main/java/org/apache/amoro/exception/AccessDeniedException.java b/amoro-common/src/main/java/org/apache/amoro/exception/AccessDeniedException.java
new file mode 100644
index 0000000000..0c96288625
--- /dev/null
+++ b/amoro-common/src/main/java/org/apache/amoro/exception/AccessDeniedException.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+package org.apache.amoro.exception;
+
+public class AccessDeniedException extends AmoroRuntimeException {
+ public AccessDeniedException() {}
+
+ public AccessDeniedException(String message) {
+ super(message);
+ }
+}
diff --git a/dist/src/main/amoro-bin/conf/policy.csv b/dist/src/main/amoro-bin/conf/policy.csv
new file mode 100644
index 0000000000..17ccdd240c
--- /dev/null
+++ b/dist/src/main/amoro-bin/conf/policy.csv
@@ -0,0 +1,4 @@
+p, admin, /*, GET|POST|DELETE|PUT
+p, read_only, /*, GET
+g, admin, admin
+g, user, read_only
\ No newline at end of file
diff --git a/dist/src/main/amoro-bin/conf/rbac_model.conf b/dist/src/main/amoro-bin/conf/rbac_model.conf
new file mode 100644
index 0000000000..e9aa027118
--- /dev/null
+++ b/dist/src/main/amoro-bin/conf/rbac_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)
diff --git a/dist/src/main/amoro-bin/conf/users.csv b/dist/src/main/amoro-bin/conf/users.csv
new file mode 100644
index 0000000000..0ca163dda1
--- /dev/null
+++ b/dist/src/main/amoro-bin/conf/users.csv
@@ -0,0 +1,2 @@
+admin,admin
+user,user
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 91e62c842e..65e510b2c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -909,6 +909,11 @@
${mockito.version}
test
+
+ org.casbin
+ jcasbin
+ 1.39.0
+
@@ -1161,6 +1166,8 @@
**/Chart.lock
release/**
+ **/*.conf
+ **/*.csv