/*
* 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 gobblin.data.management.retention.action;
import java.io.IOException;
import java.util.List;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.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.FsPermission;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.typesafe.config.Config;
import gobblin.data.management.policy.VersionSelectionPolicy;
import gobblin.data.management.version.DatasetVersion;
import gobblin.data.management.version.FileStatusAware;
import gobblin.data.management.version.FileSystemDatasetVersion;
import gobblin.util.ConfigUtils;
/**
* A {@link RetentionAction} that is used to change the permissions/owner/group of a {@link FileSystemDatasetVersion}
*/
@Slf4j
public class AccessControlAction extends RetentionAction {
/**
* Optional - The permission mode to set on selected versions either in octal or symbolic format. E.g 750
*/
private static final String MODE_KEY = "mode";
/**
* Optional - The owner to set on selected versions
*/
private static final String OWNER_KEY = "owner";
/**
* Optional - The group to set on selected versions
*/
private static final String GROUP_KEY = "group";
private final Optional<FsPermission> permission;
private final Optional<String> owner;
private final Optional<String> group;
@VisibleForTesting
@Getter
private final VersionSelectionPolicy<DatasetVersion> selectionPolicy;
@VisibleForTesting
AccessControlAction(Config actionConfig, FileSystem fs, Config jobConfig) {
super(actionConfig, fs, jobConfig);
this.permission = actionConfig.hasPath(MODE_KEY) ? Optional.of(new FsPermission(actionConfig.getString(MODE_KEY))) : Optional
.<FsPermission> absent();
this.owner = Optional.fromNullable(ConfigUtils.getString(actionConfig, OWNER_KEY, null));
this.group = Optional.fromNullable(ConfigUtils.getString(actionConfig, GROUP_KEY, null));
this.selectionPolicy = createSelectionPolicy(actionConfig, jobConfig);
}
/**
* Applies {@link #selectionPolicy} on <code>allVersions</code> and modifies permission/owner to the selected {@link DatasetVersion}s
* where necessary.
* <p>
* This action only available for {@link FileSystemDatasetVersion}. It simply skips the operation if a different type
* of {@link DatasetVersion} is passed.
* </p>
* {@inheritDoc}
* @see gobblin.data.management.retention.action.RetentionAction#execute(java.util.List)
*/
@Override
public void execute(List<DatasetVersion> allVersions) throws IOException {
// Select version on which access control actions need to performed
for (DatasetVersion datasetVersion : this.selectionPolicy.listSelectedVersions(allVersions)) {
executeOnVersion(datasetVersion);
}
}
private void executeOnVersion(DatasetVersion datasetVersion) throws IOException {
// Perform action if it is a FileSystemDatasetVersion
if (datasetVersion instanceof FileSystemDatasetVersion) {
FileSystemDatasetVersion fsDatasetVersion = (FileSystemDatasetVersion) datasetVersion;
// If the version is filestatus aware, use the filestatus to ignore permissions update when the path already has
// the desired permissions
if (datasetVersion instanceof FileStatusAware) {
for (FileStatus fileStatus : ((FileStatusAware)datasetVersion).getFileStatuses()) {
if (needsPermissionsUpdate(fileStatus) || needsOwnerUpdate(fileStatus) || needsGroupUpdate(fileStatus)) {
updatePermissionsAndOwner(fileStatus.getPath());
}
}
} else {
for (Path path : fsDatasetVersion.getPaths()) {
updatePermissionsAndOwner(path);
}
}
}
}
private boolean needsPermissionsUpdate(FileStatus fileStatus) {
return this.permission.isPresent() && !this.permission.get().equals(fileStatus.getPermission());
}
private boolean needsOwnerUpdate(FileStatus fileStatus) {
return this.owner.isPresent() && !StringUtils.equals(owner.get(), fileStatus.getOwner());
}
private boolean needsGroupUpdate(FileStatus fileStatus) {
return this.group.isPresent() && !StringUtils.equals(group.get(), fileStatus.getGroup());
}
private void updatePermissionsAndOwner(Path path) throws IOException {
boolean atLeastOneOperationFailed = false;
if (this.fs.exists(path)) {
try {
// Update permissions if set in config
if (this.permission.isPresent()) {
if (!this.isSimulateMode) {
this.fs.setPermission(path, this.permission.get());
log.debug("Set permissions for {} to {}", path, this.permission.get());
} else {
log.info("Simulating set permissions for {} to {}", path, this.permission.get());
}
}
} catch (IOException e) {
log.error(String.format("Setting permissions failed on %s", path), e);
atLeastOneOperationFailed = true;
}
// Update owner and group if set in config
if (this.owner.isPresent() || this.group.isPresent()) {
if (!this.isSimulateMode) {
this.fs.setOwner(path, this.owner.orNull(), this.group.orNull());
log.debug("Set owner and group for {} to {}:{}", path, this.owner.orNull(),
this.group.orNull());
} else {
log.info("Simulating set owner and group for {} to {}:{}", path, this.owner.orNull(),
this.group.orNull());
}
}
if (atLeastOneOperationFailed) {
throw new RuntimeException(String.format(
"At least one failure happened while processing %s. Look for previous logs for failures", path));
}
}
}
}