/**
* This file is part of git-as-svn. It is subject to the license terms
* in the LICENSE file found in the top-level directory of this distribution
* and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn,
* including this file, may be copied, modified, propagated, or distributed
* except according to the terms contained in the LICENSE file.
*/
package svnserver.auth;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import svnserver.config.AclAccessConfig;
import svnserver.config.AclConfig;
import svnserver.config.GroupConfig;
import svnserver.repository.VcsAccess;
import java.util.*;
/**
* @author Marat Radchenko <marat@slonopotamus.org>
*/
public final class ACL implements VcsAccess {
@NotNull
public static final String EveryoneMarker = "*";
@NotNull
private final Set<String> groups = new HashSet<>();
@NotNull
private final Map<String, Set<String>> user2groups = new HashMap<>();
@NotNull
private final Map<String, Set<AclEntry>> path2acl = new HashMap<>();
private final boolean anonymousRead;
public ACL(@NotNull AclConfig config) {
anonymousRead = config.isAnonymousRead();
for (final GroupConfig group : config.getGroups()) {
final String name = group.getName();
if (name.isEmpty())
throw new IllegalArgumentException("Group with empty name is not a good idea");
if (group.getUsers().length == 0)
throw new IllegalArgumentException("Group is empty: " + name);
if (!groups.add(name))
throw new IllegalArgumentException("Duplicate group found: " + name);
for (String user : group.getUsers())
user2groups.computeIfAbsent(user, s -> new HashSet<>()).add(name);
}
if (config.getAccess().length == 0)
throw new IllegalArgumentException("Empty ACL");
for (AclAccessConfig access : config.getAccess()) {
final String path = access.getPath();
if (!path.startsWith("/"))
throw new IllegalArgumentException("ACL must start with slash (/): " + path);
if (path.endsWith("/") && path.length() > 1)
throw new IllegalArgumentException("ACL must not end with slash (/): " + path);
if (access.getAllowed().length == 0)
throw new IllegalArgumentException("ACL is empty: " + path);
if (path2acl.get(path) != null)
throw new IllegalArgumentException("Duplicate ACL: " + path);
for (String allowed : access.getAllowed())
addAccess(path, allowed);
}
}
private void addAccess(@NotNull String path, @NotNull String allowed) {
final AclEntry entry;
if (allowed.equals(EveryoneMarker))
entry = new EveryoneAclEntry();
else if (allowed.startsWith("@")) {
final String group = allowed.substring(1, allowed.length());
if (!groups.contains(group))
throw new IllegalArgumentException("ACL entry " + path + " uses unknown group: " + group);
entry = new GroupAclEntry(group);
} else {
entry = new UserAclEntry(allowed);
}
if (!path2acl.computeIfAbsent(path, s -> new HashSet<>()).add(entry))
throw new IllegalArgumentException("Duplicate ACL entry " + path + ": " + allowed);
}
@Override
public void checkRead(@NotNull User user, @Nullable String path) throws SVNException {
if (user.isAnonymous() && !anonymousRead) {
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED, "Anonymous access not allowed"));
}
if (path != null) {
String toCheck = path;
while (!toCheck.isEmpty()) {
if (doCheck(user, toCheck))
return;
toCheck = toCheck.substring(0, toCheck.lastIndexOf('/'));
}
if (doCheck(user, "/"))
return;
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED, "You're not authorized to access " + path));
}
}
@Override
public void checkWrite(@NotNull User user, @Nullable String path) throws SVNException {
if (user.isAnonymous()) {
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.RA_NOT_AUTHORIZED, "Anonymous user have not write access"));
}
checkRead(user, path);
}
private boolean doCheck(@NotNull User user, @NotNull String path) {
for (AclEntry entry : path2acl.getOrDefault(path, Collections.<AclEntry>emptySet()))
if (entry.allows(user.getUserName()))
return true;
return false;
}
private interface AclEntry {
boolean allows(@NotNull String user);
}
private class GroupAclEntry implements AclEntry {
@NotNull
private final String group;
private GroupAclEntry(@NotNull String group) {
this.group = group;
}
@Override
public boolean allows(@NotNull String user) {
return user2groups.getOrDefault(user, Collections.emptySet()).contains(group);
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof GroupAclEntry && group.equals(((GroupAclEntry) o).group);
}
@Override
public int hashCode() {
return group.hashCode();
}
}
private class UserAclEntry implements AclEntry {
@NotNull
private final String user;
private UserAclEntry(@NotNull String user) {
this.user = user;
}
@Override
public boolean allows(@NotNull String user) {
return user.equals(this.user);
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof UserAclEntry && user.equals(((UserAclEntry) o).user);
}
@Override
public int hashCode() {
return user.hashCode();
}
}
private class EveryoneAclEntry implements AclEntry {
private EveryoneAclEntry() {
}
@Override
public boolean allows(@NotNull String user) {
return true;
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof EveryoneAclEntry;
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
}