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