blob: f140a788d5de3419f3e9878ba41fbdafefe54d1b [file] [log] [blame]
/*
* 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.iotdb.commons.auth.user;
import org.apache.iotdb.commons.auth.entity.PathPrivilege;
import org.apache.iotdb.commons.auth.entity.User;
import org.apache.iotdb.commons.conf.CommonDescriptor;
import org.apache.iotdb.commons.conf.IoTDBConstant;
import org.apache.iotdb.commons.file.SystemFileFactory;
import org.apache.iotdb.commons.utils.FileUtils;
import org.apache.iotdb.commons.utils.IOUtils;
import org.apache.iotdb.commons.utils.TestOnly;
import org.apache.thrift.TException;
import org.apache.tsfile.utils.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
* This class store user info in separate sequential files and every user will have one user file
* and an optional user_role file. user file schema: username-length int32| username-bytes-utf8|
* userpwd-length int32 | userpwd-bytes-utf8| system-perivileges int32| path[1]-length int32|
* path[1]-bytes-utf8 | privilege-int32| path[2]-length int32| path[2]-bytes-utf8 | privilege-int32|
* path[3]-length int32| path[3]-bytes-utf8 | privilege-int32|
*
* <p>user_role file schema: role1-length int32 | role1-bytes-utf8 | role2-length int32 |
* role2-bytes-utf8... .....
*
* <p>system-privileges is an int32 mask code. Low 16 bits for privileges and high 16 bits mark that
* whether the corresponding position 's privilege has grant option.So we can use a int32 to mark 16
* kinds of privileges. Here are the permissions corresponding to the bit location identifier:
* Manage_database : 0 MANAGE_USER : 1 MANAGE_ROLE : 2 USE_TRIGGER : 3 USE_UDF : 4 USE_CQ : 5
* USE_PIPE : 6 EXTEDN_TEMPLATE : 7 AUDIT : 8 MAINTAIN : 9 |0000-0000-0000-0001|0000-0000-0000-0001|
* means this role has Manage_database's privilege and be able to grant it to others.
*
* <p>path-privileges is a pair contains paths and privileges mask. Path privilege is a int32 mask
* code which has the same structure as system-privileges. For path privilege, the low 16 bits stand
* for four privileges, they are: READ_DATA : 0 WRITE_DATA : 1 READ_SCHEMA : 2 WRITE_SCHEMA : 3 And
* the high 16 bits stand for grant option.
*
* <p>user files and user_role files are appendable.
*/
/**
* All user/role file store in config node's user/role folder. when our system start, we load all
* user/role from folder. If we did some alter query, we just store raft log.
*/
public class LocalFileUserAccessor implements IUserAccessor {
private static final Logger LOGGER = LoggerFactory.getLogger(LocalFileUserAccessor.class);
private static final String TEMP_SUFFIX = ".temp";
private static final String STRING_ENCODING = "utf-8";
private static final String USER_SNAPSHOT_FILE_NAME = "system" + File.separator + "users";
public static final String ROLE_SUFFIX = "_role";
private final String userDirPath;
/**
* Reused buffer for primitive types encoding/decoding, which aim to reduce memory fragments. Use
* ThreadLocal for thread safety.
*/
private final ThreadLocal<ByteBuffer> encodingBufferLocal = new ThreadLocal<>();
private final ThreadLocal<byte[]> strBufferLocal = new ThreadLocal<>();
public LocalFileUserAccessor(String userDirPath) {
this.userDirPath = userDirPath;
}
/**
* Deserialize a user from its user file.
*
* @param username The name of the user to be deserialized.
* @return The user object or null if no such user.
*/
@Override
public User loadUser(String username) throws IOException {
File userProfile =
SystemFileFactory.INSTANCE.getFile(
userDirPath + File.separator + username + IoTDBConstant.PROFILE_SUFFIX);
if (!userProfile.exists() || !userProfile.isFile()) {
// System may crush before a newer file is renamed.
File newProfile =
SystemFileFactory.INSTANCE.getFile(
userDirPath + File.separator + username + IoTDBConstant.PROFILE_SUFFIX + TEMP_SUFFIX);
if (newProfile.exists() && newProfile.isFile()) {
if (!newProfile.renameTo(userProfile)) {
LOGGER.error("New profile renaming not succeed.");
}
userProfile = newProfile;
} else {
return null;
}
}
FileInputStream inputStream = new FileInputStream(userProfile);
try (DataInputStream dataInputStream =
new DataInputStream(new BufferedInputStream(inputStream))) {
User user = new User();
Pair<String, Boolean> result =
IOUtils.readAuthString(dataInputStream, STRING_ENCODING, strBufferLocal);
boolean oldVersion = result.getRight();
user.setName(result.getLeft());
user.setPassword(IOUtils.readString(dataInputStream, STRING_ENCODING, strBufferLocal));
if (oldVersion) {
IOUtils.loadRolePrivilege(user, dataInputStream, STRING_ENCODING, strBufferLocal);
int roleNum = dataInputStream.readInt();
List<String> roleList = new ArrayList<>();
for (int i = 0; i < roleNum; i++) {
String roleName = IOUtils.readString(dataInputStream, STRING_ENCODING, strBufferLocal);
roleList.add(roleName);
}
user.setRoleList(roleList);
try {
user.setUseWaterMark(dataInputStream.readInt() != 0);
} catch (EOFException e1) {
user.setUseWaterMark(false);
}
return user;
} else {
user.setSysPrivilegeSet(dataInputStream.readInt());
List<PathPrivilege> pathPrivilegeList = new ArrayList<>();
for (int i = 0; dataInputStream.available() != 0; i++) {
pathPrivilegeList.add(
IOUtils.readPathPrivilege(dataInputStream, STRING_ENCODING, strBufferLocal, false));
}
user.setPrivilegeList(pathPrivilegeList);
File roleOfUser =
SystemFileFactory.INSTANCE.getFile(
userDirPath,
File.separator + username + ROLE_SUFFIX + IoTDBConstant.PROFILE_SUFFIX);
if (!roleOfUser.isFile() || !roleOfUser.exists()) {
// System may crush before a newer file is renamed.
File newRoleProfile =
SystemFileFactory.INSTANCE.getFile(
userDirPath
+ File.separator
+ username
+ "_role"
+ IoTDBConstant.PROFILE_SUFFIX
+ TEMP_SUFFIX);
if (newRoleProfile.exists() && newRoleProfile.isFile()) {
if (!newRoleProfile.renameTo(roleOfUser)) {
LOGGER.warn(" New role profile renaming not succeed.");
}
roleOfUser = newRoleProfile;
}
}
List<String> roleList = new ArrayList<>();
if (roleOfUser.exists()) {
inputStream = new FileInputStream(roleOfUser);
try (DataInputStream roleInpuStream =
new DataInputStream(new BufferedInputStream(inputStream))) {
for (int i = 0; roleInpuStream.available() != 0; i++) {
String rolename = IOUtils.readString(roleInpuStream, STRING_ENCODING, strBufferLocal);
roleList.add(rolename);
}
}
}
user.setRoleList(roleList);
return user;
}
} catch (Exception e) {
throw new IOException(e);
} finally {
strBufferLocal.remove();
}
}
/**
* Serialize the user object to a temp file, then replace the old user file with the new file.
*
* @param user The user object that is to be saved.
*/
@Override
public void saveUser(User user) throws IOException {
File userProfile =
SystemFileFactory.INSTANCE.getFile(
userDirPath
+ File.separator
+ user.getName()
+ IoTDBConstant.PROFILE_SUFFIX
+ TEMP_SUFFIX);
try (FileOutputStream fileOutputStream = new FileOutputStream(userProfile);
BufferedOutputStream outputStream = new BufferedOutputStream(fileOutputStream)) {
// for IOTDB 1.2, the username's length will be stored as a negative number.
byte[] strBuffer = user.getName().getBytes(STRING_ENCODING);
IOUtils.writeInt(outputStream, -1 * strBuffer.length, encodingBufferLocal);
outputStream.write(strBuffer);
IOUtils.writeString(outputStream, user.getPassword(), STRING_ENCODING, encodingBufferLocal);
IOUtils.writeInt(outputStream, user.getAllSysPrivileges(), encodingBufferLocal);
int privilegeNum = user.getPathPrivilegeList().size();
for (int i = 0; i < privilegeNum; i++) {
PathPrivilege pathPrivilege = user.getPathPrivilegeList().get(i);
IOUtils.writePathPrivilege(
outputStream, pathPrivilege, STRING_ENCODING, encodingBufferLocal);
}
outputStream.flush();
fileOutputStream.getFD().sync();
} catch (Exception e) {
LOGGER.warn("Get exception when save user {}'s privileges", user.getName(), e);
throw new IOException(e);
} finally {
encodingBufferLocal.remove();
}
File roleProfile =
SystemFileFactory.INSTANCE.getFile(
userDirPath
+ File.separator
+ user.getName()
+ ROLE_SUFFIX
+ IoTDBConstant.PROFILE_SUFFIX
+ TEMP_SUFFIX);
try (FileOutputStream fileOutputStream = new FileOutputStream(roleProfile);
BufferedOutputStream outputStream = new BufferedOutputStream(fileOutputStream)) {
int userNum = user.getRoleList().size();
for (int i = 0; i < userNum; i++) {
IOUtils.writeString(
outputStream, user.getRoleList().get(i), STRING_ENCODING, encodingBufferLocal);
}
outputStream.flush();
fileOutputStream.getFD().sync();
} catch (Exception e) {
LOGGER.warn("Get Exception when save user {}'s roles", user.getName(), e);
throw new IOException(e);
} finally {
encodingBufferLocal.remove();
}
File oldUserFile =
SystemFileFactory.INSTANCE.getFile(
userDirPath + File.separator + user.getName() + IoTDBConstant.PROFILE_SUFFIX);
IOUtils.replaceFile(userProfile, oldUserFile);
File oldURoleFile =
SystemFileFactory.INSTANCE.getFile(
userDirPath
+ File.separator
+ user.getName()
+ ROLE_SUFFIX
+ IoTDBConstant.PROFILE_SUFFIX);
IOUtils.replaceFile(roleProfile, oldURoleFile);
}
/**
* Delete a user's user file.
*
* @param username The name of the user to be deleted.
* @return True if the file is successfully deleted, false if the file does not exists.
* @throws IOException when the file cannot be deleted.
*/
@Override
public boolean deleteUser(String username) throws IOException {
File userProfile =
SystemFileFactory.INSTANCE.getFile(
userDirPath + File.separator + username + IoTDBConstant.PROFILE_SUFFIX);
File backFile =
SystemFileFactory.INSTANCE.getFile(
userDirPath + File.separator + username + IoTDBConstant.PROFILE_SUFFIX + TEMP_SUFFIX);
// The user may be not flush. So if not find the file, just return true;
if (!userProfile.exists() && !backFile.exists()) {
return true;
}
if ((userProfile.exists() && !userProfile.delete())
|| (backFile.exists() && !backFile.delete())) {
throw new IOException(String.format("Cannot delete user file of %s", username));
}
if (!deleteURole(username)) {
return false;
}
return true;
}
public boolean deleteURole(String username) throws IOException {
File uRoleProfile =
SystemFileFactory.INSTANCE.getFile(
userDirPath + File.separator + username + ROLE_SUFFIX + IoTDBConstant.PROFILE_SUFFIX);
File backProfile =
SystemFileFactory.INSTANCE.getFile(
userDirPath
+ File.separator
+ username
+ ROLE_SUFFIX
+ IoTDBConstant.PROFILE_SUFFIX
+ TEMP_SUFFIX);
// This role don't have any role.
if (!uRoleProfile.exists() && !backProfile.exists()) {
return true;
}
if ((uRoleProfile.exists() && !uRoleProfile.delete())
|| (backProfile.exists() && !backProfile.delete())) {
throw new IOException(String.format("Catch error when delete %s 's role", username));
}
return true;
}
@Override
public List<String> listAllUsers() {
File userDir = SystemFileFactory.INSTANCE.getFile(userDirPath);
String[] names =
userDir.list(
(dir, name) ->
(name.endsWith(IoTDBConstant.PROFILE_SUFFIX)
&& !name.endsWith(ROLE_SUFFIX + IoTDBConstant.PROFILE_SUFFIX))
|| (name.endsWith(TEMP_SUFFIX) && !name.endsWith(ROLE_SUFFIX + TEMP_SUFFIX)));
List<String> retList = new ArrayList<>();
if (names != null) {
// in very rare situations, normal file and backup file may exist at the same time
// so a set is used to deduplicate
Set<String> set = new HashSet<>();
for (String fileName : names) {
set.add(fileName.replace(IoTDBConstant.PROFILE_SUFFIX, "").replace(TEMP_SUFFIX, ""));
}
retList.addAll(set);
}
return retList;
}
@Override
public boolean processTakeSnapshot(File snapshotDir) throws TException, IOException {
SystemFileFactory systemFileFactory = SystemFileFactory.INSTANCE;
File userFolder = systemFileFactory.getFile(userDirPath);
File userSnapshotDir = systemFileFactory.getFile(snapshotDir, USER_SNAPSHOT_FILE_NAME);
File userTmpSnapshotDir =
systemFileFactory.getFile(userSnapshotDir.getAbsolutePath() + "-" + UUID.randomUUID());
boolean result = true;
try {
result = FileUtils.copyDir(userFolder, userTmpSnapshotDir);
result &= userTmpSnapshotDir.renameTo(userSnapshotDir);
} finally {
if (userTmpSnapshotDir.exists() && !userTmpSnapshotDir.delete()) {
FileUtils.deleteFileOrDirectory(userTmpSnapshotDir);
}
}
return result;
}
@Override
public void processLoadSnapshot(File snapshotDir) throws TException, IOException {
SystemFileFactory systemFileFactory = SystemFileFactory.INSTANCE;
File userFolder = systemFileFactory.getFile(userDirPath);
File userTmpFolder =
systemFileFactory.getFile(userFolder.getAbsolutePath() + "-" + UUID.randomUUID());
File userSnapshotDir = systemFileFactory.getFile(snapshotDir, USER_SNAPSHOT_FILE_NAME);
try {
org.apache.commons.io.FileUtils.moveDirectory(userFolder, userTmpFolder);
if (!FileUtils.copyDir(userSnapshotDir, userFolder)) {
LOGGER.error("Failed to load user folder snapshot and rollback.");
// rollback if failed to copy
FileUtils.deleteFileOrDirectory(userFolder);
org.apache.commons.io.FileUtils.moveDirectory(userTmpFolder, userFolder);
}
} finally {
FileUtils.deleteFileOrDirectory(userTmpFolder);
}
}
@Override
public void reset() {
checkOldUserDir(SystemFileFactory.INSTANCE.getFile(userDirPath));
if (SystemFileFactory.INSTANCE.getFile(userDirPath).mkdirs()) {
LOGGER.info("user info dir {} is created", userDirPath);
} else if (!SystemFileFactory.INSTANCE.getFile(userDirPath).exists()) {
LOGGER.error("user info dir {} can not be created", userDirPath);
}
}
private void checkOldUserDir(File newDir) {
File oldDir = new File(CommonDescriptor.getInstance().getConfig().getOldUserFolder());
if (oldDir.exists()) {
if (!FileUtils.moveFileSafe(oldDir, newDir)) {
LOGGER.error("move old user dir fail: {}", oldDir.getAbsolutePath());
}
}
}
@Override
public String getDirPath() {
return userDirPath;
}
@Override
public void cleanUserFolder() {
File[] files = SystemFileFactory.INSTANCE.getFile(userDirPath).listFiles();
if (files != null) {
for (File file : files) {
FileUtils.deleteFileIfExist(file);
}
} else {
LOGGER.warn("User folder not exists");
}
}
@TestOnly
public void saveUserOldVersion(User user) throws IOException {
File userProfile =
SystemFileFactory.INSTANCE.getFile(
userDirPath
+ File.separator
+ user.getName()
+ IoTDBConstant.PROFILE_SUFFIX
+ TEMP_SUFFIX);
try (BufferedOutputStream outputStream =
new BufferedOutputStream(Files.newOutputStream(userProfile.toPath()))) {
try {
IOUtils.writeString(outputStream, user.getName(), STRING_ENCODING, encodingBufferLocal);
IOUtils.writeString(outputStream, user.getPassword(), STRING_ENCODING, encodingBufferLocal);
int privilegeNum = user.getPathPrivilegeList().size();
IOUtils.writeInt(outputStream, privilegeNum, encodingBufferLocal);
for (int i = 0; i < privilegeNum; i++) {
PathPrivilege pathPrivilege = user.getPathPrivilegeList().get(i);
IOUtils.writeString(
outputStream,
pathPrivilege.getPath().getFullPath(),
STRING_ENCODING,
encodingBufferLocal);
IOUtils.writeInt(outputStream, pathPrivilege.getPrivileges().size(), encodingBufferLocal);
for (Integer item : pathPrivilege.getPrivileges()) {
IOUtils.writeInt(outputStream, item, encodingBufferLocal);
}
}
int userNum = user.getRoleList().size();
IOUtils.writeInt(outputStream, userNum, encodingBufferLocal);
for (int i = 0; i < userNum; i++) {
IOUtils.writeString(
outputStream, user.getRoleList().get(i), STRING_ENCODING, encodingBufferLocal);
}
IOUtils.writeInt(outputStream, user.isUseWaterMark() ? 1 : 0, encodingBufferLocal);
outputStream.flush();
} catch (Exception e) {
throw new IOException(e);
}
} finally {
encodingBufferLocal.remove();
}
File oldFile =
SystemFileFactory.INSTANCE.getFile(
userDirPath + File.separator + user.getName() + IoTDBConstant.PROFILE_SUFFIX);
IOUtils.replaceFile(userProfile, oldFile);
}
}