diff --git a/docs/en/deployment/hadoop_java_sdk.md b/docs/en/deployment/hadoop_java_sdk.md index 539babd234ae..8742cde907e7 100644 --- a/docs/en/deployment/hadoop_java_sdk.md +++ b/docs/en/deployment/hadoop_java_sdk.md @@ -751,7 +751,6 @@ JuiceFS currently supports path permission control by integrating with Apache Ra |-----------------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `juicefs.ranger-rest-url` | | `ranger`'s HTTP link url. Not configured means not using this feature. | | `juicefs.ranger-service-name` | | `ranger`'s `service name` in `HDFS` module, required | -| `juicefs.ranger-cache-dir` | | `ranger`'s policies cache path. By default, a `UUID` path hierarchy is added under the environment variable `java.io.tmpdir` to prevent multitasking from interfering with each other. After configuring a fixed directory, multiple tasks will share the cache, and only one JuiceFS is responsible for cache refreshing, to reduce the pressure on connecting to `Ranger Admin`. | | `juicefs.ranger-poll-interval-ms` | `30000` | `ranger`'s interval to refresh cache, default is 30s | ### 2. Dependencies diff --git a/docs/zh_cn/deployment/hadoop_java_sdk.md b/docs/zh_cn/deployment/hadoop_java_sdk.md index f3c76b121b4e..9400ba952e87 100644 --- a/docs/zh_cn/deployment/hadoop_java_sdk.md +++ b/docs/zh_cn/deployment/hadoop_java_sdk.md @@ -876,7 +876,6 @@ JuiceFS 当前支持对接 Apache Ranger 的 `HDFS` 模块进行路径的权限 |-----------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------| | `juicefs.ranger-rest-url` | | `ranger`连接地址。不配置该参数即不使用该功能。 | | `juicefs.ranger-service-name` | | `ranger`中配置的`service name`,必填 | -| `juicefs.ranger-cache-dir` | | `ranger`策略的缓存路径。默认在环境变量`java.io.tmpdir`下,添加`UUID`路径层级防止多任务相互影响。当配置固定目录后,多个任务会共享缓存,有且仅有一个JuiceFS对象负责缓存刷新,减少对连接`Ranger Admin`压力。 | | `juicefs.ranger-poll-interval-ms` | `30000` | `ranger`缓存刷新周期,默认30s | ### 2. 环境及依赖 diff --git a/sdk/java/libjfs/main.go b/sdk/java/libjfs/main.go index c1dad91ae24a..cb1854b778db 100644 --- a/sdk/java/libjfs/main.go +++ b/sdk/java/libjfs/main.go @@ -695,6 +695,20 @@ func jfs_update_uid_grouping(cname, uidstr *C.char, grouping *C.char) { } } +//export jfs_getGroups +func jfs_getGroups(name, user string) string { + fslock.Lock() + defer fslock.Unlock() + userGroups := userGroupCache[name] + if userGroups != nil { + gs := userGroups[user] + if gs != nil { + return strings.Join(gs, ",") + } + } + return "" +} + //export jfs_term func jfs_term(pid int, h int64) int { w := F(h) diff --git a/sdk/java/pom.xml b/sdk/java/pom.xml index dd251c07ad90..3f9b4c715274 100644 --- a/sdk/java/pom.xml +++ b/sdk/java/pom.xml @@ -82,6 +82,14 @@ com.google.common io.juicefs.shaded.com.google.common + + org.apache.commons.lang + io.juicefs.shaded.org.apache.commons.lang + + + com.kstruct.gethostname4j + io.juicefs.shaded.com.kstruct.gethostname4j + @@ -350,6 +358,11 @@ + + commons-lang + commons-lang + 2.6 + org.apache.ranger ranger-plugins-common @@ -361,6 +374,21 @@ + + com.kstruct + gethostname4j + 0.0.2 + + + com.sun.jersey + jersey-bundle + 1.19.3 + + + org.codehaus.jackson + jackson-jaxrs + 1.9.13 + org.apache.ranger ranger-plugins-audit @@ -372,11 +400,6 @@ - - org.apache.httpcomponents - httpclient - 4.5.13 - diff --git a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java index fc590febf602..9f501677a3ba 100644 --- a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java +++ b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java @@ -93,8 +93,6 @@ static String loadVersion() { private Path workingDir; private String name; private String user; - private String group; - private Set groups; private String superuser; private String supergroup; private URI uri; @@ -102,8 +100,9 @@ static String loadVersion() { private int minBufferSize; private int cacheReplica; private boolean fileChecksumEnabled; - private static boolean permissionCheckEnabled = false; private final boolean isSuperGroupFileSystem; + private boolean isBackGroundTask = false; + private JuiceFileSystemImpl superGroupFileSystem; private RangerPermissionChecker rangerPermissionChecker; private static Libjfs lib = loadLibrary(); @@ -207,6 +206,8 @@ public static interface Libjfs { int jfs_setfacl(long pid, long h, String path, int acltype, Pointer b, int len); + String jfs_getGroups(String volName, String user); + void jfs_set_callback(LogCallBack callBack); interface LogCallBack { @@ -369,22 +370,18 @@ public void initialize(URI uri, Configuration conf) throws IOException { minBufferSize = conf.getInt("juicefs.min-buffer-size", 128 << 10); cacheReplica = Integer.parseInt(getConf(conf, "cache-replica", "1")); fileChecksumEnabled = Boolean.parseBoolean(getConf(conf, "file.checksum", "false")); - permissionCheckEnabled = getConf(conf, "ranger-rest-url", null) != null; this.ugi = UserGroupInformation.getCurrentUser(); user = ugi.getShortUserName(); - group = "nogroup"; - String groupingFile = getConf(conf, "groups", null); - if (isEmpty(groupingFile) && ugi.getGroupNames().length > 0) { - group = String.join(",", ugi.getGroupNames()); + String groupStr = "nogroup"; + if (ugi.getGroupNames().length > 0) { + groupStr = String.join(",", ugi.getGroupNames()); } - groups = Arrays.stream(group.split(",")).collect(Collectors.toSet()); superuser = getConf(conf, "superuser", "hdfs"); supergroup = getConf(conf, "supergroup", conf.get("dfs.permissions.superusergroup", "supergroup")); - if (permissionCheckEnabled && isSuperGroupFileSystem) { - group = supergroup; - groups.clear(); - groups.add(supergroup); + isBackGroundTask = conf.getBoolean("juicefs.internal-bg-task", false); + if (isSuperGroupFileSystem || isBackGroundTask) { + groupStr = supergroup; } synchronized (JuiceFileSystemImpl.class) { @@ -445,12 +442,11 @@ public void initialize(URI uri, Configuration conf) throws IOException { obj.put("freeSpace", getConf(conf, "free-space", "0.1")); obj.put("accessLog", getConf(conf, "access-log", "")); String jsonConf = obj.toString(2); - handle = lib.jfs_init(name, jsonConf, user, group, superuser, supergroup); + handle = lib.jfs_init(name, jsonConf, user, groupStr, superuser, supergroup); if (handle <= 0) { throw new IOException("JuiceFS initialized failed for jfs://" + name); } - boolean asBgTask = conf.getBoolean("juicefs.internal-bg-task", false); - if (asBgTask) { + if (isBackGroundTask) { LOG.debug("background fs {}|({})", name, handle); } else { BgTaskUtil.register(name, handle); @@ -500,36 +496,30 @@ public void initialize(URI uri, Configuration conf) throws IOException { JuiceFSInstrumentation.init(this, statistics); } - if (permissionCheckEnabled) { - try { - if (!isSuperGroupFileSystem) { - RangerConfig rangerConfig = checkAndGetRangerParams(conf); - Configuration superConf = new Configuration(conf); - superGroupFileSystem = new JuiceFileSystemImpl(true); - superGroupFileSystem.initialize(uri, superConf); - rangerPermissionChecker = new RangerPermissionChecker(superGroupFileSystem, rangerConfig, user, group); - } - } catch (Exception e) { - if (rangerPermissionChecker != null) { - rangerPermissionChecker.cleanUp(); - } - throw new RuntimeException("The initialization of the Permission Checker has failed. ", e); - } + + String rangerRestUrl = getConf(conf, "ranger-rest-url", null); + if (!isEmpty(rangerRestUrl) && !isSuperGroupFileSystem && !isBackGroundTask) { + RangerConfig rangerConfig = checkAndGetRangerParams(rangerRestUrl, conf); + Configuration superConf = new Configuration(conf); + superConf.set("juicefs.internal-bg-task", "true"); + superGroupFileSystem = new JuiceFileSystemImpl(true); + superGroupFileSystem.initialize(uri, superConf); + rangerPermissionChecker = RangerPermissionChecker.acquire(name, handle, superGroupFileSystem, rangerConfig); } - if (!asBgTask && !isSuperGroupFileSystem) { + if (!isBackGroundTask && !isSuperGroupFileSystem) { // use juicefs.users and juicefs.groups for global mapping String uidFile = getConf(conf, "users", null); - if (!isEmpty(uidFile) || !isEmpty(groupingFile)) { + String groupFile = getConf(conf, "groups", null); + if (!isEmpty(uidFile) || !isEmpty(groupFile)) { BgTaskUtil.putTask(name, "Refresh guid", () -> { - updateUidAndGrouping(uidFile, groupingFile); + updateUidAndGrouping(uidFile, groupFile); }, 1, 1, TimeUnit.MINUTES); } } } - private RangerConfig checkAndGetRangerParams(Configuration conf) throws RuntimeException, IOException { - String rangerRestUrl = getConf(conf, "ranger-rest-url", ""); + private RangerConfig checkAndGetRangerParams(String rangerRestUrl, Configuration conf) throws IOException { if (!rangerRestUrl.startsWith("http")) { throw new IOException("illegal value for parameter 'juicefs.ranger-rest-url': " + rangerRestUrl); } @@ -539,38 +529,52 @@ private RangerConfig checkAndGetRangerParams(Configuration conf) throws RuntimeE throw new IOException("illegal value for parameter 'juicefs.ranger-service-name': " + serviceName); } - String cacheDir = getConf(conf, "ranger-cache-dir", System.getProperty("java.io.tmpdir") + "/" + UUID.randomUUID()); String pollIntervalMs = getConf(conf, "ranger-poll-interval-ms", "30000"); - return new RangerConfig(rangerRestUrl, serviceName, cacheDir, pollIntervalMs); + return new RangerConfig(rangerRestUrl, serviceName, Long.parseLong(pollIntervalMs)); } private JuiceFileSystemImpl(boolean isSuperGroupFileSystem) { this.isSuperGroupFileSystem = isSuperGroupFileSystem; } + private Set getGroups() { + String groupsFile = getConf(getConf(), "groups", null); + if (isEmpty(groupsFile)) { + return new HashSet<>(ugi.getGroups()); + } + String gStr = lib.jfs_getGroups(name, user); + Set res; + if (!isEmpty(gStr)) { + res = new HashSet<>(Arrays.asList(gStr.split(","))) ; + } else { + res = new HashSet<>(ugi.getGroups()); + } + return res; + } + private boolean hasSuperPermission() { - return user.equals(superuser) || groups.contains(supergroup); + return user.equals(superuser) || getGroups().contains(supergroup); } private boolean needCheckPermission() { - return permissionCheckEnabled && !hasSuperPermission(); + return rangerPermissionChecker != null && !isSuperGroupFileSystem && !isBackGroundTask && !hasSuperPermission() ; } private boolean checkPathAccess(Path path, FsAction action, String operation) throws IOException { - return rangerPermissionChecker.checkPermission(path, false, null, null, action, operation); + return rangerPermissionChecker.checkPermission(path, false, null, null, action, operation, user, getGroups()); } private boolean checkParentPathAccess(Path path, FsAction action, String operation) throws IOException { - return rangerPermissionChecker.checkPermission(path, false, null, action, null, operation); + return rangerPermissionChecker.checkPermission(path, false, null, action, null, operation, user, getGroups()); } private boolean checkAncestorAccess(Path path, FsAction action, String operation) throws IOException { - return rangerPermissionChecker.checkPermission(path, false, action, null, null, operation); + return rangerPermissionChecker.checkPermission(path, false, action, null, null, operation, user, getGroups()); } private boolean checkOwner(Path path, String operation) throws IOException { - return rangerPermissionChecker.checkPermission(path, true, null, null, null, operation); + return rangerPermissionChecker.checkPermission(path, true, null, null, null, operation, user, getGroups()); } private boolean isEmpty(String str) { @@ -623,7 +627,6 @@ private void updateUidAndGrouping(String uidFile, String groupFile) throws IOExc } lib.jfs_update_uid_grouping(name, uidstr, grouping); - groups = Arrays.stream(group.split(",")).collect(Collectors.toSet()); } private void initializeStorageIds(Configuration conf) throws IOException { @@ -1858,6 +1861,7 @@ public void setTimes(Path p, long mtime, long atime) throws IOException { @Override public void close() throws IOException { super.close(); + RangerPermissionChecker.release(name, handle); BgTaskUtil.unregister(name, handle, () -> { cachedHostsForName.clear(); hashForName.clear(); @@ -1868,9 +1872,6 @@ public void close() throws IOException { if (metricsEnable) { JuiceFSInstrumentation.close(); } - if (rangerPermissionChecker != null) { - rangerPermissionChecker.cleanUp(); - } } @Override diff --git a/sdk/java/src/main/java/io/juicefs/permission/LockFileChecker.java b/sdk/java/src/main/java/io/juicefs/permission/LockFileChecker.java deleted file mode 100644 index 055eb73751b7..000000000000 --- a/sdk/java/src/main/java/io/juicefs/permission/LockFileChecker.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * JuiceFS, Copyright 2024 Juicedata, Inc. - * - * Licensed 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 io.juicefs.permission; - -import java.io.File; -import java.io.IOException; - -public class LockFileChecker { - - public static boolean checkAndCreateLockFile(String directoryPath) { - File directory = new File(directoryPath); - - if (!directory.exists()) { - directory.mkdirs(); - } - - File lockFile = new File(directory, ".lock"); - - if (lockFile.exists()) { - return false; - } else { - try { - lockFile.createNewFile(); - return true; - } catch (IOException e) { - throw new RuntimeException("ranger policies cache dir cannot created. ", e); - } - } - } - - public static void cleanUp(String directoryPath) { - File directory = new File(directoryPath + ".lock"); - directory.deleteOnExit(); - } - -} diff --git a/sdk/java/src/main/java/io/juicefs/permission/RangerAdminRefresher.java b/sdk/java/src/main/java/io/juicefs/permission/RangerAdminRefresher.java new file mode 100644 index 000000000000..0595b06eb573 --- /dev/null +++ b/sdk/java/src/main/java/io/juicefs/permission/RangerAdminRefresher.java @@ -0,0 +1,254 @@ +/* + * JuiceFS, Copyright 2024 Juicedata, Inc. + * + * Licensed 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 io.juicefs.permission; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.hadoop.fs.*; +import org.apache.ranger.admin.client.RangerAdminClient; +import org.apache.ranger.plugin.contextenricher.RangerTagEnricher; +import org.apache.ranger.plugin.service.RangerBasePlugin; +import org.apache.ranger.plugin.util.RangerRoles; +import org.apache.ranger.plugin.util.RangerServiceNotFoundException; +import org.apache.ranger.plugin.util.ServicePolicies; +import org.apache.ranger.plugin.util.ServiceTags; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.Comparator; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + + +public class RangerAdminRefresher { + private static final Logger LOG = LoggerFactory.getLogger(RangerAdminRefresher.class); + + private static final String JFS_RANGER_DIR = "/.sys/ranger"; + + private RangerBasePlugin plugIn; + private Path rangerDir; + private Path rangerRulePath; + private long lastMtime; + private final long pollingIntervalMs; + + private final RangerAdminClient rangerAdmin; + private final Gson gson = new GsonBuilder().setDateFormat("yyyyMMdd-HH:mm:ss.SSS-Z").create(); + private long lastKnownPolicyVersion = -1L; + private long lastPolicyActivationTimeInMillis; + private long lastKnownRoleVersion = -1L; + private long lastRoleActivationTimeInMillis; + private long lastKnownTagVersion = -1L; + private long lastTagActivationTimeInMillis; + + private final FileSystem fs; + private final ScheduledExecutorService refreshThread; + + public RangerAdminRefresher(RangerBasePlugin plugIn, RangerAdminClient rangerAdmin, FileSystem fs, String rangerUrl, long pollingIntervalMs) { + + this.plugIn = plugIn; + this.rangerAdmin = rangerAdmin; + this.fs = fs; + String serviceName = plugIn.getServiceName(); + URI uri = URI.create(rangerUrl); + String rangerDirName = uri.getHost().replace(".", "_") + "_" + uri.getPort() + "_" + serviceName; + this.rangerDir = new Path(JFS_RANGER_DIR, rangerDirName); + this.rangerRulePath = new Path(rangerDir, "rules"); + this.refreshThread = Executors.newScheduledThreadPool(1, r -> { + Thread t = new Thread(r, "JuiceFS Ranger Refresher"); + t.setDaemon(true); + return t; + }); + this.pollingIntervalMs = pollingIntervalMs; + } + + public void start() { + loadRangerItem(); + refreshThread.scheduleAtFixedRate(this::loadRangerItem, pollingIntervalMs, pollingIntervalMs, TimeUnit.MILLISECONDS); + } + + /** + * 1. read rules from jfs + * 2. choose one client to check ranger admin, if updated, download and save rules to jfs + */ + public void loadRangerItem() { + RangerRules rangerRules = null; + // try to load rules from jfs + try { + rangerRules = loadRangerRules(); + } catch (IOException e) { + LOG.debug("Load ranger rules failed", e); + } + + if (rangerRules != null) { + if (updateRules(rangerRules.getPolicies(), rangerRules.getTags(), rangerRules.getRoles())) { + LOG.info("Ranger rules has been updated, use new rules from juicefs"); + } + } + + boolean checkUpdate = checkUpdate(pollingIntervalMs); + // load rules from ranger admin + if (rangerRules == null || checkUpdate) { + ServicePolicies policiesFromRanger = null; + ServiceTags tagsFromRanger = null; + RangerRoles rolesFromRanger = null; + try { + policiesFromRanger = rangerAdmin.getServicePoliciesIfUpdated(lastKnownPolicyVersion, lastPolicyActivationTimeInMillis); + tagsFromRanger = rangerAdmin.getServiceTagsIfUpdated(lastKnownTagVersion, lastTagActivationTimeInMillis); + rolesFromRanger = rangerAdmin.getRolesIfUpdated(lastKnownRoleVersion, lastRoleActivationTimeInMillis); + } catch (RangerServiceNotFoundException e) { + LOG.warn("Ranger service not found", e); + } catch (Exception e) { + LOG.warn("Load policies from ranger failed", e); + } + if (updateRules(policiesFromRanger, tagsFromRanger, rolesFromRanger)) { + if (checkUpdate) { + try { + ServicePolicies p = rangerRules != null ? rangerRules.getPolicies() : null; + ServiceTags t = rangerRules != null ? rangerRules.getTags() : null; + RangerRoles r = rangerRules != null ? rangerRules.getRoles() : null; + if (policiesFromRanger != null) { + LOG.info("ServicePolicies updated from Ranger Admin"); + p = policiesFromRanger; + } + if (tagsFromRanger != null) { + LOG.info("ServiceTags updated from Ranger Admin"); + t = tagsFromRanger; + } + if (rolesFromRanger != null) { + LOG.info("RangerRoles updated from Ranger Admin"); + r = rolesFromRanger; + } + saveRangerRules(new RangerRules(p, t, r)); + } catch (IOException e) { + LOG.warn("Save rules to juicefs failed", e); + } + } + } + } + } + + private boolean checkUpdate(long pollingIntervalMs) { + try { + boolean exists = fs.exists(rangerDir); + if (!exists) { + fs.mkdirs(rangerDir); + } + FileStatus[] lockFiles = fs.listStatus(rangerDir, path -> { + String name = path.getName(); + return name.endsWith(".lock"); + }); + String prefix = String.valueOf((System.currentTimeMillis() / pollingIntervalMs) * pollingIntervalMs); + Path lockPath = new Path(rangerDir, prefix + ".lock"); + if (lockFiles == null || lockFiles.length == 0) { + try (FSDataOutputStream ignore = fs.create(lockPath, false)) { + return true; + } + } else { + if (lockFiles.length > 1) { + Arrays.sort(lockFiles, Comparator.comparing(o -> o.getPath().getName())); + } + if (lockFiles[lockFiles.length - 1].getPath().getName().compareTo(lockPath.getName()) >= 0) { + return false; + } + try (FSDataOutputStream ignore = fs.create(lockPath, false)) { + for (FileStatus lockFile : lockFiles) { + fs.delete(lockFile.getPath(), false); + } + return true; + } + } + } catch (FileAlreadyExistsException ignored) { + return false; + } + catch (IOException e) { + LOG.warn("Check update failed", e); + return false; + } + } + + private void saveRangerRules(RangerRules rules) throws IOException { + String rulesJson = gson.toJson(rules, RangerRules.class); + byte[] bytes = rulesJson.getBytes(); + try (FSDataOutputStream out = fs.create(rangerRulePath)) { + out.write(bytes); + } catch (FileNotFoundException e) { + fs.mkdirs(rangerRulePath.getParent()); + try (FSDataOutputStream out = fs.create(rangerRulePath)) { + out.write(bytes); + } + } + } + + private RangerRules loadRangerRules() throws IOException { + FileStatus fileStatus = fs.getFileStatus(rangerRulePath); + long mtime = fileStatus.getModificationTime(); + if (lastMtime == mtime) { + return null; + } + try (FSDataInputStream in = fs.open(rangerRulePath)) { + byte[] bytes = new byte[(int) fileStatus.getLen()]; + in.readFully(bytes); + String rulesJson = new String(bytes); + RangerRules rangerRules = gson.fromJson(rulesJson, RangerRules.class); + lastMtime = mtime; + return rangerRules; + } + } + + private boolean updateRules(ServicePolicies newSvcPolicies, ServiceTags newTags, RangerRoles newRangerRoles) { + boolean updated = false; + if (newSvcPolicies != null) { + long policyVersion = newSvcPolicies.getPolicyVersion() == null ? -1 : newSvcPolicies.getPolicyVersion(); + if (lastKnownPolicyVersion != policyVersion) { + plugIn.setPolicies(newSvcPolicies); + lastKnownPolicyVersion = policyVersion; + lastPolicyActivationTimeInMillis = System.currentTimeMillis(); + updated = true; + } + } + if (newTags != null) { + long tagVersion = newTags.getTagVersion() == null ? -1 : newTags.getTagVersion(); + if (lastKnownTagVersion != tagVersion) { + RangerTagEnricher tagEnricher = plugIn.getTagEnricher(); + if (tagEnricher != null) { + tagEnricher.setServiceTags(newTags); + } + lastKnownTagVersion = tagVersion; + lastTagActivationTimeInMillis = System.currentTimeMillis(); + updated = true; + } + } + if (newRangerRoles != null) { + long roleVersion = newRangerRoles.getRoleVersion() == null ? -1 : newRangerRoles.getRoleVersion(); + if (lastKnownRoleVersion != roleVersion) { + plugIn.setRoles(newRangerRoles); + lastKnownRoleVersion = roleVersion; + lastRoleActivationTimeInMillis = System.currentTimeMillis(); + updated = true; + } + } + return updated; + } + + public void stop() { + refreshThread.shutdownNow(); + } +} diff --git a/sdk/java/src/main/java/io/juicefs/permission/RangerConfig.java b/sdk/java/src/main/java/io/juicefs/permission/RangerConfig.java index 4259047953e9..f13573a5812d 100644 --- a/sdk/java/src/main/java/io/juicefs/permission/RangerConfig.java +++ b/sdk/java/src/main/java/io/juicefs/permission/RangerConfig.java @@ -18,21 +18,17 @@ public class RangerConfig { - public RangerConfig(String rangerRestUrl, String serviceName, String cacheDir, String pollIntervalMs) { + public RangerConfig(String rangerRestUrl, String serviceName, long pollIntervalMs) { this.rangerRestUrl = rangerRestUrl; this.serviceName = serviceName; this.pollIntervalMs = pollIntervalMs; - this.cacheDir = cacheDir; } private String rangerRestUrl; private String serviceName; - private String pollIntervalMs = "30000"; - - private String cacheDir; - + private long pollIntervalMs; public String getRangerRestUrl() { return rangerRestUrl; @@ -50,20 +46,11 @@ public void setServiceName(String serviceName) { this.serviceName = serviceName; } - - public String getCacheDir() { - return cacheDir; - } - - public void setCacheDir(String cacheDir) { - this.cacheDir = cacheDir; - } - - public String getPollIntervalMs() { + public long getPollIntervalMs() { return pollIntervalMs; } - public void setPollIntervalMs(String pollIntervalMs) { + public void setPollIntervalMs(long pollIntervalMs) { this.pollIntervalMs = pollIntervalMs; } diff --git a/sdk/java/src/main/java/io/juicefs/permission/RangerJfsPlugin.java b/sdk/java/src/main/java/io/juicefs/permission/RangerJfsPlugin.java new file mode 100644 index 000000000000..d23c7b7ac4b2 --- /dev/null +++ b/sdk/java/src/main/java/io/juicefs/permission/RangerJfsPlugin.java @@ -0,0 +1,72 @@ +/* + * JuiceFS, Copyright 2024 Juicedata, Inc. + * + * Licensed 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 io.juicefs.permission; + +import io.juicefs.utils.ReflectionUtil; +import org.apache.hadoop.fs.FileSystem; +import org.apache.ranger.admin.client.RangerAdminClient; +import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig; +import org.apache.ranger.plugin.service.RangerBasePlugin; +import org.apache.ranger.plugin.service.RangerChainedPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class RangerJfsPlugin extends RangerBasePlugin { + private static final Logger LOG = LoggerFactory.getLogger(RangerJfsPlugin.class); + + private FileSystem fs; + private String rangerUrl; + private RangerAdminRefresher refresher; + private long pollingIntervalMs; + + public RangerJfsPlugin(FileSystem fs, String serviceName, String rangerUrl, long pollingIntervalMs) { + super(new RangerPluginCfg("hdfs", serviceName, "jfs", null, null, null)); + this.fs = fs; + this.rangerUrl = rangerUrl; + RangerPluginConfig config = getConfig(); + config.addResource(fs.getConf()); + this.pollingIntervalMs = pollingIntervalMs; + } + + @Override + public void init() { + cleanup(); + RangerAdminClient admin = createAdminClient(getConfig()); + refresher = new RangerAdminRefresher(this, admin, fs, rangerUrl, pollingIntervalMs); + refresher.start(); + List chainedPlugins = null; + try { + chainedPlugins = (List) ReflectionUtil.getField(RangerBasePlugin.class.getName(), "chainedPlugins", this); + } catch (Exception e) { + LOG.warn("Get field \"chainedPlugins\" failed", e); + } + if (chainedPlugins != null) { + for (RangerChainedPlugin plugin : chainedPlugins) { + plugin.init(); + } + } + } + + @Override + public void cleanup() { + super.cleanup(); + if (refresher != null) { + refresher.stop(); + } + } +} diff --git a/sdk/java/src/main/java/io/juicefs/permission/RangerPermissionChecker.java b/sdk/java/src/main/java/io/juicefs/permission/RangerPermissionChecker.java index b205d7d3f5f2..d437aee74bc7 100644 --- a/sdk/java/src/main/java/io/juicefs/permission/RangerPermissionChecker.java +++ b/sdk/java/src/main/java/io/juicefs/permission/RangerPermissionChecker.java @@ -17,23 +17,23 @@ package io.juicefs.permission; import com.google.common.collect.Sets; -import io.juicefs.JuiceFileSystemImpl; import org.apache.commons.lang.StringUtils; import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.permission.FsAction; import org.apache.hadoop.security.AccessControlException; -import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig; import org.apache.ranger.plugin.policyengine.RangerAccessResult; -import org.apache.ranger.plugin.policyengine.RangerPolicyEngineOptions; -import org.apache.ranger.plugin.service.RangerBasePlugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.FileNotFoundException; import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; /** * for auth checker @@ -44,6 +44,9 @@ public class RangerPermissionChecker { private static final Logger LOG = LoggerFactory.getLogger(RangerPermissionChecker.class); + private static final Map pcs = new ConcurrentHashMap<>(); + private static final Map> runningInstance = new HashMap<>(); + private final HashMap> fsAction2ActionMapper = new HashMap>() { { put(FsAction.NONE, new HashSet<>()); @@ -57,51 +60,64 @@ public class RangerPermissionChecker { } }; - private final JuiceFileSystemImpl superGroupFileSystem; - - private final String user; - - private final Set groups; - - private final String rangerCacheDir; - - private final RangerBasePlugin rangerPlugin; - - private static final String RANGER_SERVICE_TYPE = "hdfs"; + private final FileSystem superGroupFileSystem; + private final RangerJfsPlugin rangerPlugin; - public RangerPermissionChecker(JuiceFileSystemImpl superGroupFileSystem, RangerConfig config, String user, String group) { + private RangerPermissionChecker(FileSystem superGroupFileSystem, RangerConfig config) { this.superGroupFileSystem = superGroupFileSystem; - this.user = user; - this.groups = Arrays.stream(group.split(",")).collect(Collectors.toSet()); - - this.rangerCacheDir = config.getCacheDir(); - boolean startRangerRefresher = LockFileChecker.checkAndCreateLockFile(rangerCacheDir); - - RangerPluginConfig rangerPluginContext = buildRangerPluginContext(RANGER_SERVICE_TYPE, config.getServiceName(), startRangerRefresher); - rangerPlugin = new RangerBasePlugin(rangerPluginContext); - rangerPlugin.getConfig().set("ranger.plugin.hdfs.policy.cache.dir", this.rangerCacheDir); + rangerPlugin = new RangerJfsPlugin(superGroupFileSystem, config.getServiceName(), config.getRangerRestUrl(), config.getPollIntervalMs()); rangerPlugin.getConfig().set("ranger.plugin.hdfs.service.name", config.getServiceName()); rangerPlugin.getConfig().set("ranger.plugin.hdfs.policy.rest.url", config.getRangerRestUrl()); + rangerPlugin.getConfig().setIsFallbackSupported(true); rangerPlugin.init(); } - protected RangerPolicyEngineOptions buildRangerPolicyEngineOptions(boolean startRangerRefresher) { - if (startRangerRefresher) { - return null; + public static RangerPermissionChecker acquire(String volName, long handle, FileSystem superGroupFileSystem, RangerConfig config) throws IOException { + synchronized (runningInstance) { + if (!runningInstance.containsKey(volName)) { + if (pcs.containsKey(volName)) { + throw new IOException("RangerPermissionChecker for volume: " + volName + " is already created, but no running instance found."); + } + RangerPermissionChecker pc = new RangerPermissionChecker(superGroupFileSystem, config); + pcs.put(volName, pc); + Set handles = new HashSet<>(); + handles.add(handle); + runningInstance.put(volName, handles); + return pc; + } else { + RangerPermissionChecker pc = pcs.get(volName); + if (pc == null) { + throw new IOException("RangerPermissionChecker for volume: " + volName + " is already created, but no instance found."); + } + runningInstance.get(volName).add(handle); + return pc; + } } - LOG.info("Other JuiceFS Client is refreshing ranger policy, will close the refresher here."); - RangerPolicyEngineOptions options = new RangerPolicyEngineOptions(); - options.disablePolicyRefresher = true; - return options; } - protected RangerPluginConfig buildRangerPluginContext(String serviceType, String serviceName, boolean startRangerRefresher) { - return new RangerPluginConfig(serviceType, serviceName, serviceName, - null, null, buildRangerPolicyEngineOptions(startRangerRefresher)); + public static void release(String volName, long handle) { + if (handle <= 0) { + return; + } + synchronized (runningInstance) { + if (!runningInstance.containsKey(volName)) { + return; + } + Set handles = runningInstance.get(volName); + boolean removed = handles.remove(handle); + if (!removed) { + return; + } + if (handles.size() == 0) { + RangerPermissionChecker pc = pcs.remove(volName); + pc.cleanUp(); + runningInstance.remove(volName); + } + } } public boolean checkPermission(Path path, boolean checkOwner, FsAction ancestorAccess, FsAction parentAccess, - FsAction access, String operationName) throws IOException { + FsAction access, String operationName, String user, Set groups) throws IOException { RangerPermissionContext context = new RangerPermissionContext(user, groups, operationName); PathObj obj = path2Obj(path); @@ -157,7 +173,11 @@ public void cleanUp() { } catch (Exception e) { LOG.warn("Error when clean up ranger plugin threads.", e); } - LockFileChecker.cleanUp(rangerCacheDir); + try { + superGroupFileSystem.close(); + } catch (Exception e) { + LOG.warn("Error when close super group file system.", e); + } } private static boolean checkResult(AuthzStatus authzStatus, String user, String action, String path) throws AccessControlException { @@ -191,19 +211,12 @@ private AuthzStatus isAccessAllowed(FileStatus file, FsAction access, RangerPerm String pathOwner = file.getOwner(); AuthzStatus authzStatus = null; for (String accessType : accessTypes) { - RangerJfsAccessRequest request = new RangerJfsAccessRequest(path, pathOwner, accessType, context.operationName, user, context.userGroups); + RangerJfsAccessRequest request = new RangerJfsAccessRequest(path, pathOwner, accessType, context.operationName, context.user, context.userGroups); LOG.debug(request.toString()); - - RangerAccessResult result = null; - try { - result = rangerPlugin.isAccessAllowed(request); - if (result != null) { - LOG.debug(result.toString()); - } - } catch (Throwable e) { - throw new RuntimeException("Check Permission Error. ", e); + RangerAccessResult result = rangerPlugin.isAccessAllowed(request); + if (result != null) { + LOG.debug(result.toString()); } - if (result == null || !result.getIsAccessDetermined()) { authzStatus = AuthzStatus.NOT_DETERMINED; } else if (!result.getIsAllowed()) { diff --git a/sdk/java/src/main/java/io/juicefs/permission/RangerPluginCfg.java b/sdk/java/src/main/java/io/juicefs/permission/RangerPluginCfg.java new file mode 100644 index 000000000000..c169edf7b7c1 --- /dev/null +++ b/sdk/java/src/main/java/io/juicefs/permission/RangerPluginCfg.java @@ -0,0 +1,78 @@ +/* + * JuiceFS, Copyright 2024 Juicedata, Inc. + * + * Licensed 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 io.juicefs.permission; + +import org.apache.ranger.authorization.hadoop.config.RangerConfiguration; +import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig; +import org.apache.ranger.plugin.policyengine.RangerPolicyEngineOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; + +public class RangerPluginCfg extends RangerPluginConfig { + private static final Logger LOG = LoggerFactory.getLogger(RangerPluginCfg.class); + + @Override + public boolean addResourceIfReadable(String aResourceName) { + URL fUrl = this.getFileLocation(aResourceName); + if (fUrl != null) { + try { + this.addResource(fUrl); + } catch (Exception e) { + LOG.error("Unable to load the resource name [" + aResourceName + "]. Ignoring the resource:" + fUrl); + } + } + return true; + } + + public static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + + private URL getFileLocation(String fileName) { + URL lurl = null; + if (!isEmpty(fileName)) { + lurl = RangerConfiguration.class.getClassLoader().getResource(fileName); + + if (lurl == null ) { + lurl = RangerConfiguration.class.getClassLoader().getResource("/" + fileName); + } + + if (lurl == null ) { + File f = new File(fileName); + if (f.exists()) { + try { + lurl=f.toURI().toURL(); + } catch (MalformedURLException e) { + LOG.error("Unable to load the resource name [" + fileName + "]. Ignoring the resource:" + f.getPath()); + } + } else { + if(LOG.isDebugEnabled()) { + LOG.debug("Conf file path " + fileName + " does not exists"); + } + } + } + } + return lurl; + } + + public RangerPluginCfg(String serviceType, String serviceName, String appId, String clusterName, String clusterType, RangerPolicyEngineOptions policyEngineOptions) { + super(serviceType, serviceName, appId, clusterName, clusterType, policyEngineOptions); + } +} \ No newline at end of file diff --git a/sdk/java/src/main/java/io/juicefs/permission/RangerRules.java b/sdk/java/src/main/java/io/juicefs/permission/RangerRules.java new file mode 100644 index 000000000000..ac7faa375e6b --- /dev/null +++ b/sdk/java/src/main/java/io/juicefs/permission/RangerRules.java @@ -0,0 +1,61 @@ +/* + * JuiceFS, Copyright 2024 Juicedata, Inc. + * + * Licensed 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 io.juicefs.permission; + +import org.apache.ranger.plugin.util.RangerRoles; +import org.apache.ranger.plugin.util.ServicePolicies; +import org.apache.ranger.plugin.util.ServiceTags; + +import java.io.Serializable; + +public class RangerRules implements Serializable { + private ServicePolicies policies; + private ServiceTags tags; + private RangerRoles roles; + + public RangerRules() { + } + + public RangerRules(ServicePolicies policies, ServiceTags tags, RangerRoles roles) { + this.policies = policies; + this.tags = tags; + this.roles = roles; + } + + public ServicePolicies getPolicies() { + return policies; + } + + public void setPolicies(ServicePolicies policies) { + this.policies = policies; + } + + public ServiceTags getTags() { + return tags; + } + + public void setTags(ServiceTags tags) { + this.tags = tags; + } + + public RangerRoles getRoles() { + return roles; + } + + public void setRoles(RangerRoles roles) { + this.roles = roles; + } +} diff --git a/sdk/java/src/main/java/io/juicefs/utils/ReflectionUtil.java b/sdk/java/src/main/java/io/juicefs/utils/ReflectionUtil.java index 418be1736022..9bedcc24a278 100644 --- a/sdk/java/src/main/java/io/juicefs/utils/ReflectionUtil.java +++ b/sdk/java/src/main/java/io/juicefs/utils/ReflectionUtil.java @@ -16,6 +16,7 @@ package io.juicefs.utils; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; public class ReflectionUtil { public static boolean hasMethod(String className, String method, String[] params) { @@ -50,4 +51,11 @@ public static Constructor getConstructor(Class clazz, Class... para return null; } } + + public static Object getField(String className, String field, Object obj) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + Class clazz = Class.forName(className); + Field f = clazz.getDeclaredField(field); + f.setAccessible(true); + return f.get(obj); + } } diff --git a/sdk/java/src/main/resources/ranger-hdfs-audit.xml b/sdk/java/src/main/resources/ranger-hdfs-audit.xml deleted file mode 100644 index 24856dcb7f76..000000000000 --- a/sdk/java/src/main/resources/ranger-hdfs-audit.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - xasecure.audit.is.enabled - false - - \ No newline at end of file diff --git a/sdk/java/src/main/resources/ranger-hdfs-security.xml b/sdk/java/src/main/resources/ranger-hdfs-security.xml deleted file mode 100644 index 2ef9a06309d9..000000000000 --- a/sdk/java/src/main/resources/ranger-hdfs-security.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - ranger.plugin.hdfs.service.name - xxx - - Name of the Ranger service containing policies for this YARN instance - - - - - ranger.plugin.hdfs.policy.source.impl - org.apache.ranger.admin.client.RangerAdminRESTClient - - Class to retrieve policies from the source - - - - - ranger.plugin.hdfs.policy.rest.url - xxx - - URL to Ranger Admin - - - - - ranger.plugin.hdfs.policy.pollIntervalMs - 30000 - - How often to poll for changes in policies? - - - - - ranger.plugin.hdfs.policy.cache.dir - xxx - - Directory where Ranger policies are cached after successful retrieval from the source - - - - - ranger.plugin.hdfs.policy.rest.client.connection.timeoutMs - 120000 - - Hdfs Plugin RangerRestClient Connection Timeout in Milli Seconds - - - - - ranger.plugin.hdfs.policy.rest.client.read.timeoutMs - 30000 - - Hdfs Plugin RangerRestClient read Timeout in Milli Seconds - - - - - xasecure.add-hadoop-authorization - true - - Enable/Disable the default hadoop authorization (based on - rwxrwxrwx permission on the resource) if Ranger Authorization fails. - - - \ No newline at end of file