Gitlab Community Edition Instance

Commit 948d2983 authored by mhellka's avatar mhellka
Browse files

Added FileRealm (incomplete)

parent 4b7a9983
Pipeline #111690 failed with stage
in 2 minutes and 11 seconds
......@@ -29,5 +29,13 @@
<artifactId>cdstar-config</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package de.gwdg.cdstar.auth.realm;
import java.io.Console;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import de.gwdg.cdstar.Utils;
import de.gwdg.cdstar.auth.Credentials;
import de.gwdg.cdstar.auth.KnownPrincipalCredentials;
import de.gwdg.cdstar.auth.Permission;
import de.gwdg.cdstar.auth.Principal;
import de.gwdg.cdstar.auth.Session;
import de.gwdg.cdstar.auth.StringPermission;
import de.gwdg.cdstar.auth.UsernamePasswordCredentials;
import de.gwdg.cdstar.auth.simple.Account;
import de.gwdg.cdstar.auth.simple.QName;
import de.gwdg.cdstar.auth.simple.SimpleAuthorizer;
import de.gwdg.cdstar.runtime.Config;
import de.gwdg.cdstar.runtime.ConfigException;
import de.gwdg.cdstar.runtime.Plugin;
/**
* A {@link SimpleAuthorizer} subclass that can read and store its state from
* yaml files.
*/
@Plugin
public class FileRealm implements Authorizer, Authenticator, GroupResolver {
private static final Logger log = LoggerFactory.getLogger(FileRealm.class);
private static final ObjectMapper yaml = new YAMLMapper();
private final class ConfigDirVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (Files.isHidden(dir))
return FileVisitResult.SKIP_SUBTREE;
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (Files.isHidden(file))
return FileVisitResult.CONTINUE;
if (!(file.getFileName().toString().endsWith(".yaml") || file.getFileName().toString().endsWith(".yml")))
return FileVisitResult.CONTINUE;
load(file);
return FileVisitResult.CONTINUE;
}
}
private final class ConfigSource {
Path file;
FileConfig config;
String domain;
Map<QName, FileAccount> accounts;
Map<QName, Set<QName>> groupRoles;
Map<QName, Set<StringPermission>> roles;
ConfigSource(Path src, FileConfig cfg) {
String srcString = file.toAbsolutePath().toString();
domain = Utils.notNullOrEmpty(cfg.domain) ? cfg.domain : defaultDomain;
accounts = new HashMap<>(cfg.users.size());
groupRoles = new HashMap<>(cfg.groups.size());
roles = new HashMap<>(cfg.roles.size());
cfg.users.forEach((name, user) -> {
FileAccount acc = getAccount(QName.fromString(name, domain));
if (Utils.notNullOrEmpty(user.password)) {
int index;
if ((index = user.password.indexOf(":")) == -1)
throw new IllegalArgumentException("Invalid pasword hash for user: " + name);
final byte[] key = Utils.base64decode(user.password.substring(0, index));
final byte[] hash = Utils.base64decode(user.password.substring(index + 1));
acc.withPassword(hash, key);
}
if (Utils.notNullOrEmpty(user.roles))
user.roles.forEach(roleName -> acc.withRole(QName.fromString(roleName, acc.getDomain())));
if (Utils.notNullOrEmpty(user.permissions))
user.permissions.stream().map(StringPermission::parse).forEach(acc::withPermission);
});
cfg.groups.forEach((name, grp) -> {
QName groupName = QName.fromString(name, domain);
if (Utils.notNullOrEmpty(grp.roles)) {
Set<QName> roleSet = groupRoles.computeIfAbsent(groupName, gn -> new HashSet<>());
grp.roles.forEach(r -> {
QName roleName = QName.fromString(r, groupName.getDomain());
getRole(roleName);
roleSet.add(roleName);
});
}
if (Utils.notNullOrEmpty(grp.members)) {
grp.members.forEach(member -> {
getAccount(QName.fromString(member, groupName.getDomain())).withGroup(groupName);
});
}
});
cfg.roles.forEach((name, role) -> {
Set<StringPermission> roleSet = getRole(QName.fromString(name, domain));
role.permissions.stream().map(StringPermission::parse).forEach(roleSet::add);
});
}
private FileAccount getAccount(QName name) {
return accounts.computeIfAbsent(name, qname -> new FileAccount(this, qname));
}
private Set<StringPermission> getRole(QName name) {
return roles.computeIfAbsent(name, r -> new HashSet<>());
}
}
final static class FileConfig {
String domain;
Map<String, FileUser> users;
Map<String, FileRole> roles;
Map<String, FileGroup> groups;
final static class FileUser {
String password;
Set<String> permissions;
Set<String> roles;
// Map<String, String> properties;
}
final static class FileGroup {
Set<String> members;
Set<String> roles;
}
final static class FileRole {
// String description;
Set<String> permissions;
}
}
private String name;
private String defaultDomain;
private List<ConfigSource> sources = new ArrayList<>();
private Map<QName, Set<QName>> groups;
public FileRealm(Config config) throws ConfigException, IOException {
name = config.get("_name", "file");
defaultDomain = config.get("domain", name);
load(Paths.get(config.get("source")));
}
public FileRealm(String name, String domain) throws ConfigException, IOException {
this.name = name;
this.defaultDomain = domain;
}
private void load(Path source) throws IOException {
if (Files.isDirectory(source)) {
try {
Files.walkFileTree(source, new ConfigDirVisitor());
} catch (IOException e) {
throw new IOException("Unable to scan config directory: "+source, e);
}
return;
}
try {
ConfigSource src = new ConfigSource(source, yaml.readValue(source.toFile(), FileConfig.class));
// register/replace and flush caches
} catch (IOException e) {
throw new IOException("Failed to load config file: "+source, e);
}
}
@Override
public String getName() {
return name;
}
@Override
public boolean isMemberOf(Session session, String groupName, String groupDomain) {
final FileAccount acc = resolveAccount(session);
if (acc == null)
return false;
QName qgroup = new QName(groupName, groupDomain != null ? groupDomain : defaultDomain);
return groups.getOrDefault(qgroup, Collections.emptySet()).contains(acc.qname)
|| acc.isMemberOf(new QName(groupName, groupDomain));
}
private FileAccount resolveAccount(Session session) {
if (session == null)
return null;
if (session instanceof FileSession) {
final FileSession fs = (FileSession) session;
return fs.account.realm == this ? fs.account : null;
}
return resolveAccount(session.getPrincipal().getId(), session.getPrincipal().getDomain());
}
private FileAccount resolveAccount(String name, String domain) {
return accounts.get(new QName(name, domain != null ? domain : defaultDomain));
}
@Override
public Session login(Credentials request) {
if (request instanceof UsernamePasswordCredentials) {
final UsernamePasswordCredentials creds = (UsernamePasswordCredentials) request;
final FileAccount account = resolveAccount(creds.getName(), creds.getDomain());
if (account != null && account.checkPassword(creds.getPassword()))
return new FileSession(account, false);
}
if (request instanceof KnownPrincipalCredentials) {
final Principal principal = ((KnownPrincipalCredentials) request).getPrincipal();
if (principal instanceof FileAccount && ((FileAccount) principal).realm == this)
return new FileSession((FileAccount) principal, true);
}
return null;
}
@Override
public void logout(Session who) {
}
@Override
public boolean isPermitted(Session session, Permission p) {
// TODO Auto-generated method stub
return false;
}
private class FileAccount implements Principal {
QName name;
byte[] passwordHash;
byte[] signKey;
final Set<QName> groups = new HashSet<>();
final Set<QName> roles = new HashSet<>();
final Set<StringPermission> permissions = new HashSet<>();
final ConfigSource source;
public FileAccount(ConfigSource src, QName name) {
this.source = src;
this.name = name;
}
public FileAccount withGroup(QName group) {
groups.add(group);
return this;
}
public FileAccount withRole(QName role) {
roles.add(role);
return this;
}
public FileAccount withPassword(byte[] hash, byte[] key) {
passwordHash = Arrays.copyOf(hash, hash.length);
signKey = Arrays.copyOf(key, key.length);
return this;
}
public FileAccount withPermission(StringPermission p) {
permissions.add(p);
return this;
}
public boolean checkPassword(char[] password) {
if (passwordHash != null)
return Utils.signCheck(passwordHash, Utils.toBytes(password), signKey);
return false;
}
public boolean isMemberOf(QName qName) {
return groups.contains(qName);
}
@Override
public String getId() {
return name.getName();
}
@Override
public String getDomain() {
return name.getDomain();
}
}
private static class FileSession implements Session {
private FileAccount account;
private boolean remembered;
public FileSession(FileAccount account, boolean remembered) {
this.account = account;
this.remembered = remembered;
}
@Override
public boolean isRemembered() {
return remembered;
}
@Override
public Authenticator getAuthenticator() {
return account.realm;
}
@Override
public Principal getPrincipal() {
return account;
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment