/* * ==================================================================== * Copyright (c) 2004-2008 TMate Software Ltd. All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at http://svnkit.com/license.html. * If newer versions of this license are posted there, you may use a * newer version instead, at your option. * ==================================================================== */ package org.tmatesoft.svn.core.internal.server.dav; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.regex.Pattern; import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNProperty; import org.tmatesoft.svn.core.internal.util.SVNHashMap; import org.tmatesoft.svn.core.internal.util.SVNPathUtil; import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslatorInputStream; import org.tmatesoft.svn.util.SVNLogType; /** * @author TMate Software Ltd. * @version 1.2.0 */ public class SVNPathBasedAccess { private static final Pattern COMMA = Pattern.compile(","); private static final String ANONYMOUS_REPOSITORY = ""; public static final int SVN_ACCESS_NONE = 0; public static final int SVN_ACCESS_READ = 1; public static final int SVN_ACCESS_WRITE = 2; public static final int SVN_ACCESS_RECURSIVE = 4; private String myConfigPath; private int myCurrentLineNumber = 1; private int myCurrentLineColumn = 0; private char myUngottenChar = 0; private boolean myHasUngottenChar = false; private StringBuffer mySectionName; private StringBuffer myOption; private StringBuffer myValue; private Map myGroups; private Map myAliases; private Map myRules; public SVNPathBasedAccess(File pathBasedAccessConfiguration) throws SVNException { myConfigPath = pathBasedAccessConfiguration.getAbsolutePath(); InputStream stream = null; try { stream = new SVNTranslatorInputStream(SVNFileUtil.openFileForReading(pathBasedAccessConfiguration, SVNLogType.NETWORK), SVNProperty.EOL_LF_BYTES, true, null, false); } catch (SVNException e) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "Failed to load the AuthzSVNAccessFile: ''{0}''", pathBasedAccessConfiguration.getAbsolutePath()), SVNLogType.NETWORK); } try { parse(stream); } catch (IOException e) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getLocalizedMessage()), SVNLogType.NETWORK); } validate(); } private String getConfigPath() { return myConfigPath; } private void increaseCurrentLineNumber() { myCurrentLineNumber++; } private int getCurrentLineNumber() { return myCurrentLineNumber; } private void increaseCurrentLineColumn() { myCurrentLineColumn++; } private void resetCurrentLineColumn() { myCurrentLineColumn = 0; } private int getCurrentLineColumn() { return myCurrentLineColumn; } private char getUngottenChar() { return myUngottenChar; } private void setUngottenChar(char ungottenChar) { myUngottenChar = ungottenChar; } private boolean hasUngottenChar() { return myHasUngottenChar; } private void setHasUngottenChar(boolean hasUngottenChar) { myHasUngottenChar = hasUngottenChar; } private StringBuffer getSectionName() { if (mySectionName == null) { mySectionName = new StringBuffer(); } return mySectionName; } private StringBuffer getOption() { if (myOption == null) { myOption = new StringBuffer(); } return myOption; } private StringBuffer getValue() { if (myValue == null) { myValue = new StringBuffer(); } return myValue; } private Map getGroups() { if (myGroups == null) { myGroups = new SVNHashMap(); } return myGroups; } private boolean groupContainsUser(String group, String user) { String[] groupUsers = (String[]) getGroups().get(group); if (groupUsers == null) { return false; } for (int i = 0; i < groupUsers.length; i++) { if (groupUsers[i].startsWith("@")) { if (groupContainsUser(groupUsers[i].substring("@".length()), user)) { return true; } } else if (groupUsers[i].startsWith("&")) { if (aliasIsUser(groupUsers[i].substring("&".length()), user)) { return true; } } else if (groupUsers[i].equals(user)) { return true; } } return false; } private Map getAliases() { if (myAliases == null) { myAliases = new SVNHashMap(); } return myAliases; } private boolean aliasIsUser(String alias, String user) { String aliasValue = (String) getAliases().get(alias); return aliasValue != null && aliasValue.equals(user); } private Map getRules() { if (myRules == null) { myRules = new SVNHashMap(); } return myRules; } public boolean checkAccess(String repository, String path, String user, int access) { RepositoryAccess repositoryAccess = (RepositoryAccess) getRules().get(repository); if (repositoryAccess == null) { repositoryAccess = (SVNPathBasedAccess.RepositoryAccess) getRules().get(ANONYMOUS_REPOSITORY); if (repositoryAccess == null) { return false; } } return repositoryAccess.checkPathAccess(path, user, access); } private void parse(InputStream is) throws IOException, SVNException { boolean isEOF = false; int currentByte; do { currentByte = skipWhitespace(is); switch (currentByte) { case'[': if (getCurrentLineColumn() == 0) { parseSectionName(is); } else { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "''{0}'' : ''{1}'' : Section header must start in the first column.", new Object[]{getConfigPath(), new Integer(getCurrentLineNumber())}), SVNLogType.NETWORK); } break; case'#': if (getCurrentLineColumn() == 0) { skipToEndOfLine(is); increaseCurrentLineNumber(); } else { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "''{0}'' : ''{1}'' : Comment must start in the first column.", new Object[]{getConfigPath(), new Integer(getCurrentLineNumber())}), SVNLogType.NETWORK); } break; case'\n': increaseCurrentLineNumber(); break; case-1: isEOF = true; break; default: if (getSectionName().length() == 0) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "''{0}'' : ''{1}'' : Section header expected.", new Object[]{getConfigPath(), new Integer(getCurrentLineNumber())}), SVNLogType.NETWORK); } else if (getCurrentLineColumn() != 0) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "''{0}'' : ''{1}'' : Option expected.", new Object[]{getConfigPath(), new Integer(getCurrentLineNumber())}), SVNLogType.NETWORK); } else { parseOption(is, currentByte); } } } while (!isEOF); getSectionName().setLength(0); getOption().setLength(0); getValue().setLength(0); } private int parseSectionName(InputStream is) throws IOException, SVNException { getSectionName().setLength(0); int currentByte = getc(is); while (currentByte != -1 && currentByte != '\n' && currentByte != ']') { getSectionName().append((char) currentByte); currentByte = getc(is); } if (currentByte != ']') { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "''{0}'' : ''{1}'' : Section header must end with ']'.", new Object[]{getConfigPath(), new Integer(getCurrentLineNumber())}), SVNLogType.NETWORK); } else { currentByte = skipToEndOfLine(is); if (currentByte != -1) { increaseCurrentLineNumber(); } } return currentByte; } private int parseOption(InputStream is, int firstByte) throws IOException, SVNException { getOption().setLength(0); int currentByte = firstByte; while (currentByte != -1 && currentByte != ':' && currentByte != '=' && currentByte != '\n') { getOption().append((char) currentByte); currentByte = getc(is); } if (currentByte != ':' && currentByte != '=') { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "''{0}'' : ''{1}'' : Option must end with ':' or '='.", new Object[]{getConfigPath(), new Integer(getCurrentLineNumber())}), SVNLogType.NETWORK); } else { trimBuffer(getOption()); currentByte = parseValue(is); } return currentByte; } private int parseValue(InputStream is) throws IOException, SVNException { getValue().setLength(0); int currentByte = getc(is); boolean isEndOfValue = false; while (currentByte != -1 && currentByte != '\n') { getValue().append((char) currentByte); currentByte = getc(is); } trimBuffer(getValue()); while (true) { if (currentByte == -1 || isEndOfValue) { updateConfiguration(); break; } increaseCurrentLineNumber(); currentByte = skipWhitespace(is); switch (currentByte) { case'\n': increaseCurrentLineNumber(); isEndOfValue = true; continue; case-1: isEndOfValue = true; continue; default: if (getCurrentLineColumn() == 0) { ungetc((char) currentByte); isEndOfValue = true; } else { //Continuation line found. getValue().append(' '); while (currentByte != -1 && currentByte != '\n') { getValue().append((char) currentByte); currentByte = getc(is); } trimBuffer(getValue()); } } } return currentByte; } private int skipWhitespace(InputStream is) throws IOException { resetCurrentLineColumn(); int currentByte = getc(is); while (Character.isWhitespace((char) currentByte)) { currentByte = getc(is); increaseCurrentLineColumn(); } return currentByte; } private int skipToEndOfLine(InputStream is) throws IOException { int currentByte = getc(is); while (currentByte != -1 && currentByte != '\n') { currentByte = getc(is); resetCurrentLineColumn(); } return currentByte; } private int getc(InputStream is) throws IOException { if (hasUngottenChar()) { setHasUngottenChar(false); return getUngottenChar(); } return is.read(); } private void ungetc(char ungottenChar) { setUngottenChar(ungottenChar); setHasUngottenChar(true); } private void trimBuffer(StringBuffer buffer) { while (buffer.length() > 0 && Character.isWhitespace(buffer.charAt(0))) { buffer.deleteCharAt(0); } while (buffer.length() > 0 && Character.isWhitespace(buffer.charAt(buffer.length() - 1))) { buffer.deleteCharAt(buffer.length() - 1); } } private void updateConfiguration() throws SVNException { if ("groups".equals(getSectionName().toString())) { updateGroups(); } else if ("aliases".equals(getSectionName().toString())) { updateAliases(); } else { updateRules(); } } private void updateGroups() throws SVNException { String groupName = getOption().toString(); if (getValue().length() == 0) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "An authz rule refers to group ''{0}'', which is undefined", groupName), SVNLogType.NETWORK); } String[] users = COMMA.split(getValue()); getGroups().put(groupName, users); } private void updateAliases() throws SVNException { String alias = getOption().toString(); if (getValue().length() == 0) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "An authz rule refers to alies ''{0}'', which is undefined", alias), SVNLogType.NETWORK); } getAliases().put(alias, getValue().toString()); } private void updateRules() throws SVNException { int delimeterIndex = getSectionName().indexOf(":"); String repositoryName = delimeterIndex == -1 ? ANONYMOUS_REPOSITORY : getSectionName().substring(0, delimeterIndex); String path = delimeterIndex == -1 ? getSectionName().toString() : getSectionName().substring(delimeterIndex + 1); String value = getValue().toString(); if (getOption().charAt(0) == '~') { if (getOption().charAt(1) == '~') { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "Rule ''{0}'' has more than one inversion; double negatives are not permitted.", getOption()), SVNLogType.NETWORK); } if (getOption().charAt(1) == '*') { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "Authz rules with match string '~*' are not allowed, because they never match anyone."), SVNLogType.NETWORK); } } if (getOption().charAt(0) == '$') { String token = getOption().substring(1); if (!"anonymous".equals(token) && !"authenticated".equals(token)) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "Unrecognized authz token ''{0}''.", getOption()), SVNLogType.NETWORK); } } if (value.length() > 0 && !"r".equals(value) && !"rw".equals(value)) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "The value ''{0}'' in rule ''{1}'' is not allowed in authz rules.", new Object[]{value, getOption()}), SVNLogType.NETWORK); } RepositoryAccess repositoryAccess = (RepositoryAccess) getRules().get(repositoryName); if (repositoryAccess == null) { repositoryAccess = new RepositoryAccess(ANONYMOUS_REPOSITORY.equals(repositoryName)); getRules().put(repositoryName, repositoryAccess); } repositoryAccess.addRule(path, getOption().toString(), getValue().toString()); } private void validate() throws SVNException { Collection checkedPathes = new ArrayList(); for (Iterator iterator = getGroups().keySet().iterator(); iterator.hasNext();) { String groupName = (String) iterator.next(); checkedPathes.clear(); groupWalk(groupName, checkedPathes); } for (Iterator repositories = getRules().values().iterator(); repositories.hasNext();) { RepositoryAccess repositoryAccess = (RepositoryAccess) repositories.next(); repositoryAccess.validateRules(); } } private void groupWalk(String group, Collection checkedGroups) throws SVNException { String[] users = (String[]) getGroups().get(group); if (users == null) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "An authz rule refers to group ''{0}'', which is undefined.", group), SVNLogType.NETWORK); } for (int i = 0; i < users.length; i++) { users[i] = users[i].trim(); if (users[i].startsWith("@")) { String subGroup = users[i].substring("@".length()); if (checkedGroups.contains(subGroup)) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "Circular dependency between groups ''{0}'' and ''{1}''", new Object[]{group, subGroup}), SVNLogType.NETWORK); } checkedGroups.add(subGroup); groupWalk(subGroup, checkedGroups); checkedGroups.remove(subGroup); } else if (users[i].startsWith("&")) { String alias = users[i].substring("&".length()); if (!getAliases().keySet().contains(alias)) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "An authz rule refers to alias ''{0}'', which is undefined.", alias), SVNLogType.NETWORK); } } } } private class RepositoryAccess { boolean myAnonymous = false; private Map myPathRules; private PathAccess myGlobalAccess; private RepositoryAccess(boolean isAnonymous) { myAnonymous = isAnonymous; } private void addRule(String path, String matchString, String value) { if (path.equals("/") || path.length() == 0) { myGlobalAccess = myGlobalAccess == null ? new PathAccess() : myGlobalAccess; myGlobalAccess.addRule(matchString, value); } myPathRules = myPathRules == null ? new SVNHashMap() : myPathRules; PathAccess pathAccess = (PathAccess) myPathRules.get(path); if (pathAccess == null) { pathAccess = new PathAccess(); myPathRules.put(path, pathAccess); } pathAccess.addRule(matchString, value); } private void validateRules() throws SVNException { if (myGlobalAccess != null) { myGlobalAccess.validateRules(); } if (myPathRules != null) { for (Iterator iterator = myPathRules.values().iterator(); iterator.hasNext();) { PathAccess pathAccess = (PathAccess) iterator.next(); pathAccess.validateRules(); } } } private boolean checkPathAccess(String path, String user, int requestedAccess) { boolean accessGranted = false; if (path == null || path.length() == 0 || "/".equals(path)) { if (myGlobalAccess != null) { int[] pathAccess = myGlobalAccess.checkAccess(user); if (isAccessDetermined(pathAccess, requestedAccess)) { accessGranted = isAccessGranted(pathAccess, requestedAccess); } } } else { if (myPathRules == null) { return false; } int[] pathAccess = checkCurrentPath(path, user); if (isAccessDetermined(pathAccess, requestedAccess)) { accessGranted = isAccessGranted(pathAccess, requestedAccess); } else { String currentPath = path; while (currentPath.length() > 0 && !"/".equals(currentPath)) { currentPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.removeTail(currentPath)); pathAccess = checkCurrentPath(currentPath, user); if (isAccessDetermined(pathAccess, requestedAccess)) { accessGranted = isAccessGranted(pathAccess, requestedAccess); break; } } } } if (accessGranted && ((requestedAccess & SVN_ACCESS_RECURSIVE) != SVN_ACCESS_NONE)) { accessGranted = checkTreeAccess(user, path, requestedAccess); } return accessGranted; } private int[] checkCurrentPath(String currentPath, String user) { int[] pathAccess = new int[]{SVN_ACCESS_NONE, SVN_ACCESS_NONE}; PathAccess currentPathAccess = (PathAccess) myPathRules.get(currentPath); if (currentPathAccess != null) { pathAccess = currentPathAccess.checkAccess(user); } else if (!myAnonymous) { RepositoryAccess commonRepositoryAccess = (RepositoryAccess) getRules().get(ANONYMOUS_REPOSITORY); if (commonRepositoryAccess != null) { pathAccess = commonRepositoryAccess.checkPathAccess(user, currentPath); } } return pathAccess; } private int[] checkPathAccess(String user, String path) { int[] result = new int[]{SVN_ACCESS_NONE, SVN_ACCESS_NONE}; PathAccess pathAccess = (PathAccess) myPathRules.get(path); if (pathAccess != null) { result = pathAccess.checkAccess(user); } return result; } private boolean checkTreeAccess(String user, String path, int requestedAccess) { if (myRules == null) { return false; } boolean accessGranted = true; for (Iterator iterator = myRules.entrySet().iterator(); iterator.hasNext();) { Map.Entry entry = (Map.Entry) iterator.next(); String currentPath = (String) entry.getKey(); if (SVNPathUtil.isAncestor(path, currentPath)) { PathAccess currentPathAccess = (PathAccess) entry.getValue(); int[] pathAccess = currentPathAccess.checkAccess(user); accessGranted = isAccessGranted(pathAccess, requestedAccess) || !isAccessDetermined(pathAccess, requestedAccess); if (!accessGranted) { return accessGranted; } } } return accessGranted; } private boolean isAccessGranted(int[] pathAccess, int requestedAccess) { if (pathAccess == null) { return false; } int allow = pathAccess[0]; int deny = pathAccess[1]; int strippedAccess = requestedAccess & (SVN_ACCESS_READ | SVN_ACCESS_WRITE); return (deny & requestedAccess) == SVN_ACCESS_NONE || (allow & requestedAccess) == strippedAccess; } private boolean isAccessDetermined(int[] pathAccess, int requestedAccess) { if (pathAccess == null) { return false; } int allow = pathAccess[0]; int deny = pathAccess[1]; return ((deny & requestedAccess) != SVN_ACCESS_NONE) || ((allow & requestedAccess) != SVN_ACCESS_NONE); } } private class PathAccess { private Map myRules; private void addRule(String matchString, String value) { myRules = myRules == null ? new SVNHashMap() : myRules; myRules.put(matchString, value); } private void validateRules() throws SVNException { if (myRules != null) { for (Iterator iterator = myRules.keySet().iterator(); iterator.hasNext();) { String matchString = (String) iterator.next(); if (matchString.startsWith("@")) { if (!getGroups().keySet().contains(matchString.substring("@".length()))) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "An authz rule refers to group ''{0}'', which is undefined.", matchString), SVNLogType.NETWORK); } } else if (matchString.startsWith("&")) { if (!getAliases().keySet().contains(matchString.substring("&".length()))) { SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_DAV_INVALID_CONFIG_VALUE, "An authz rule refers to alias ''{0}'', which is undefined.", matchString), SVNLogType.NETWORK); } } } } } private int[] checkAccess(String user) { if (myRules == null) { return null; } int deny = SVN_ACCESS_NONE; int allow = SVN_ACCESS_NONE; for (Iterator iterator = myRules.entrySet().iterator(); iterator.hasNext();) { Map.Entry entry = (Map.Entry) iterator.next(); String matchString = (String) entry.getKey(); String accessType = (String) entry.getValue(); if (ruleApliesToUser(matchString, user)) { if (accessType.indexOf('r') >= 0) { allow |= SVN_ACCESS_READ; } else { deny |= SVN_ACCESS_READ; } if (accessType.indexOf('w') >= 0) { allow |= SVN_ACCESS_WRITE; } else { deny |= SVN_ACCESS_WRITE; } } } return new int[]{allow, deny}; } private boolean ruleApliesToUser(String matchString, String user) { if (matchString.startsWith("~")) { return !ruleApliesToUser(matchString.substring("~".length()), user); } if (matchString.equals("*")) { return true; } if (matchString.equals("$anonymous")) { return user == null; } if (matchString.equals("$authenticated")) { return user != null; } if (user == null) { return false; } if (matchString.startsWith("@")) { return groupContainsUser(matchString.substring("@".length()), user); } else if (matchString.startsWith("&")) { return aliasIsUser(matchString.substring("&".length()), user); } else { return matchString.equals(user); } } } }