package restx.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import restx.common.Types;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* A UserRepository implementation using 2 files to load data.
*
* One file is used to store users, in json format as a Map<String, U> read with jackson.
*
* Example:
* <pre>
* [
* {
* "name": "admin",
* "principalRoles": ["restx-admin"]
* }
* ]
* </pre>
*
* Another file is used to store user credentials.
* Storing credentials separately is a good practice, which allow to have different security policies on the
* two files, and avoid to have credentials part of the user class.
*
* Example:
* <pre>
* {
* "admin": "tuyvuicxvcx78vdsfuisd"
* }
* </pre>
*/
public class FileBasedUserRepository<U extends RestxPrincipal> implements UserRepository<U> {
private static final Logger logger = LoggerFactory.getLogger(FileBasedUserRepository.class);
private final Class<U> userClass;
private final U defaultAdmin;
private final CachedData<String> credentials;
private final CachedData<U> users;
public FileBasedUserRepository(Class<U> userClass, ObjectMapper mapper, U defaultAdmin,
Path usersPath, Path credentialsPath, boolean reloadOnChange) {
this.userClass = userClass;
this.defaultAdmin = defaultAdmin;
this.credentials = new CachedData<>(credentialsPath, "credentials",
mapper, Types.newParameterizedType(Map.class, String.class, String.class), reloadOnChange);
this.users = new CachedData<U>(usersPath, "users",
mapper, Types.newParameterizedType(List.class, userClass), reloadOnChange) {
@Override
@SuppressWarnings("unchecked")
protected Map<String, U> toMap(Object o) {
List<U> users = (List<U>) o;
Map<String, U> usersMap = new LinkedHashMap();
for (U user : users) {
usersMap.put(user.getName(), user);
}
return usersMap;
}
};
}
@Override
public Optional<U> findUserByName(String name) {
return users.get(name);
}
@Override
public Optional<String> findCredentialByUserName(String userName) {
return credentials.get(userName);
}
@Override
public boolean isAdminDefined() {
for (U u : users.data().values()) {
if (u.getPrincipalRoles().contains("restx-admin")) {
return true;
}
}
return false;
}
@Override
public U defaultAdmin() {
return defaultAdmin;
}
private static class CachedData<T> {
private final Path dataPath;
private final String name;
private final ObjectReader reader;
private final boolean reloadOnChange;
private long dataFileTimestamp;
private Map<String, T> data;
public CachedData(Path path, String name, ObjectMapper mapper, ParameterizedType valueType, boolean reloadOnChange) {
this.dataPath = path;
this.name = name;
this.reader = mapper.reader().withType(valueType);
this.reloadOnChange = reloadOnChange;
}
public Optional<T> get(String name) {
return Optional.fromNullable(data().get(name));
}
private synchronized Map<String, T> data() {
if (data == null || reloadOnChange) {
if (!dataPath.toFile().exists()) {
logger.warn(name + " file " + dataPath.toAbsolutePath() + " not found");
if (data == null) {
data = new HashMap<>();
}
} else {
if (dataFileTimestamp >= dataPath.toFile().lastModified()) {
logger.debug(name + " are up to date");
} else {
logger.debug("loading " + name + " from " + dataPath.toAbsolutePath());
try {
long lastModified = dataPath.toFile().lastModified();
data = toMap(reader.readValue(dataPath.toFile()));
dataFileTimestamp = lastModified;
} catch (IOException e) {
logger.warn("error while loading " + name + " file " + dataPath + ": " + e.getMessage(), e);
data = new HashMap<>();
}
}
}
}
return data;
}
@SuppressWarnings("unchecked")
protected Map<String, T> toMap(Object o) {
return (Map<String, T>) o;
}
}
}