From 4f390898e9f75f57bc28613f9ffb39024d15edb3 Mon Sep 17 00:00:00 2001 From: thierry Date: Fri, 21 May 2010 13:18:45 +0000 Subject: [PATCH] restrict svn access - installed at princeton --- scripts/commit-access-control.cfg | 73 ++++++ scripts/commit-access-control.pl | 411 ++++++++++++++++++++++++++++++ scripts/pre-commit | 69 +++++ 3 files changed, 553 insertions(+) create mode 100644 scripts/commit-access-control.cfg create mode 100755 scripts/commit-access-control.pl create mode 100755 scripts/pre-commit diff --git a/scripts/commit-access-control.cfg b/scripts/commit-access-control.cfg new file mode 100644 index 0000000..3864a9c --- /dev/null +++ b/scripts/commit-access-control.cfg @@ -0,0 +1,73 @@ +# This is a sample configuration file for commit-access-control.pl. +# +# $Id$ +# +# This file uses the Windows ini style, where the file consists of a +# number of sections, each section starts with a unique section name +# in square brackets. Parameters in each section are specified as +# Name = Value. Any spaces around the equal sign will be ignored. If +# there are multiple sections with exactly the same section name, then +# the parameters in those sections will be added together to produce +# one section with cumulative parameters. +# +# The commit-access-control.pl script reads these sections in order, +# so later sections may overwrite permissions granted or removed in +# previous sections. +# +# Each section has three valid parameters. Any other parameters are +# ignored. +# access = (read-only|read-write) +# +# This parameter is a required parameter. Valid values are +# `read-only' and `read-write'. +# +# The access rights to apply to modified files and directories +# that match the `match' regular expression described later on. +# +# match = PERL_REGEX +# +# This parameter is a required parameter and its value is a Perl +# regular expression. +# +# To help users that automatically write regular expressions that +# match the beginning of absolute paths using ^/, the script +# removes the / character because subversion paths, while they +# start at the root level, do not begin with a /. +# +# users = username1 [username2 [username3 [username4 ...]]] +# or +# users = username1 [username2] +# users = username3 username4 +# +# This parameter is optional. The usernames listed here must be +# exact usernames. There is no regular expression matching for +# usernames. You may specify all the usernames that apply on one +# line or split the names up on multiple lines. +# +# The access rights from `access' are applied to ALL modified +# paths that match the `match' regular expression only if NO +# usernames are specified in the section or if one of the listed +# usernames matches the author of the commit. +# +# By default, because you're using commit-access-control.pl in the +# first place to protect your repository, the script sets the +# permissions to all files and directories in the repository to +# read-only, so if you want to open up portions of the repository, +# you'll need to edit this file. +# +# NOTE: NEVER GIVE DIFFERENT SECTIONS THE SAME SECTION NAME, OTHERWISE +# THE PARAMETERS FOR THOSE SECTIONS WILL BE MERGED TOGETHER INTO ONE +# SECTION AND YOUR SECURITY MAY BE COMPROMISED. + +[Make everything read-write for all users] +match = .* +access = read-write + +[Make modules read-only for all users] +match = ^(playground|tests)/.* +access = read-only + +[An illustration of a user-based access] +match = ^tests/.* +users = thierry +access = read-write diff --git a/scripts/commit-access-control.pl b/scripts/commit-access-control.pl new file mode 100755 index 0000000..1c618c1 --- /dev/null +++ b/scripts/commit-access-control.pl @@ -0,0 +1,411 @@ +#!/usr/bin/env perl + +# ==================================================================== +# commit-access-control.pl: check if the user that submitted the +# transaction TXN-NAME has the appropriate rights to perform the +# commit in repository REPOS using the permissions listed in the +# configuration file CONF_FILE. +# +# $HeadURL$ +# $LastChangedDate$ +# $LastChangedBy$ +# $LastChangedRevision$ +# +# Usage: commit-access-control.pl REPOS TXN-NAME CONF_FILE +# +# ==================================================================== +# 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. +# ==================================================================== + +# Turn on warnings the best way depending on the Perl version. +BEGIN { + if ( $] >= 5.006_000) + { require warnings; import warnings; } + else + { $^W = 1; } +} + +use strict; +use Carp; +use Config::IniFiles 2.27; + +###################################################################### +# Configuration section. + +# Svnlook path. +my $svnlook = "/usr/bin/svnlook"; + +# Since the path to svnlook depends upon the local installation +# preferences, check that the required program exists to insure that +# the administrator has set up the script properly. +{ + my $ok = 1; + foreach my $program ($svnlook) + { + if (-e $program) + { + unless (-x $program) + { + warn "$0: required program `$program' is not executable, ", + "edit $0.\n"; + $ok = 0; + } + } + else + { + warn "$0: required program `$program' does not exist, edit $0.\n"; + $ok = 0; + } + } + exit 1 unless $ok; +} + +###################################################################### +# Initial setup/command-line handling. + +&usage unless @ARGV == 3; + +my $repos = shift; +my $txn = shift; +my $cfg_filename = shift; + +unless (-e $repos) + { + &usage("$0: repository directory `$repos' does not exist."); + } +unless (-d $repos) + { + &usage("$0: repository directory `$repos' is not a directory."); + } +unless (-e $cfg_filename) + { + &usage("$0: configuration file `$cfg_filename' does not exist."); + } +unless (-r $cfg_filename) + { + &usage("$0: configuration file `$cfg_filename' is not readable."); + } + +# Define two constant subroutines to stand for read-only or read-write +# access to the repository. +sub ACCESS_READ_ONLY () { 'read-only' } +sub ACCESS_READ_WRITE () { 'read-write' } + +###################################################################### +# Load the configuration file and validate it. +my $cfg = Config::IniFiles->new(-file => $cfg_filename); +unless ($cfg) + { + die "$0: error in loading configuration file `$cfg_filename'", + @Config::IniFiles::errors ? ":\n@Config::IniFiles::errors\n" + : ".\n"; + } + +# Go through each section of the configuration file, validate that +# each section has the required parameters and complain about unknown +# parameters. Compile any regular expressions. +my @sections = $cfg->Sections; +{ + my $ok = 1; + foreach my $section (@sections) + { + # First check for any unknown parameters. + foreach my $param ($cfg->Parameters($section)) + { + next if $param eq 'match'; + next if $param eq 'users'; + next if $param eq 'access'; + warn "$0: config file `$cfg_filename' section `$section' parameter ", + "`$param' is being ignored.\n"; + $cfg->delval($section, $param); + } + + my $access = $cfg->val($section, 'access'); + if (defined $access) + { + unless ($access eq ACCESS_READ_ONLY or $access eq ACCESS_READ_WRITE) + { + warn "$0: config file `$cfg_filename' section `$section' sets ", + "`access' to illegal value `$access'.\n"; + $ok = 0; + } + } + else + { + warn "$0: config file `$cfg_filename' section `$section' does ", + "not set `access' parameter.\n"; + $ok = 0; + } + + my $match_regex = $cfg->val($section, 'match'); + if (defined $match_regex) + { + # To help users that automatically write regular expressions + # that match the beginning of absolute paths using ^/, + # remove the / character because subversion paths, while + # they start at the root level, do not begin with a /. + $match_regex =~ s#^\^/#^#; + + my $match_re; + eval { $match_re = qr/$match_regex/ }; + if ($@) + { + warn "$0: config file `$cfg_filename' section `$section' ", + "`match' regex `$match_regex' does not compile:\n$@\n"; + $ok = 0; + } + else + { + $cfg->newval($section, 'match_re', $match_re); + } + } + else + { + warn "$0: config file `$cfg_filename' section `$section' does ", + "not set `match' parameter.\n"; + $ok = 0; + } + } + exit 1 unless $ok; +} + +###################################################################### +# Harvest data using svnlook. + +# Change into /tmp so that svnlook diff can create its .svnlook +# directory. +my $tmp_dir = '/tmp'; +chdir($tmp_dir) + or die "$0: cannot chdir `$tmp_dir': $!\n"; + +# Get the author from svnlook. +my @svnlooklines = &read_from_process($svnlook, 'author', $repos, '-t', $txn); +my $author = shift @svnlooklines; +unless (length $author) + { + die "$0: txn `$txn' has no author.\n"; + } + +# Figure out what directories have changed using svnlook.. +my @dirs_changed = &read_from_process($svnlook, 'dirs-changed', $repos, + '-t', $txn); + +# Lose the trailing slash in the directory names if one exists, except +# in the case of '/'. +my $rootchanged = 0; +for (my $i=0; $i<@dirs_changed; ++$i) + { + if ($dirs_changed[$i] eq '/') + { + $rootchanged = 1; + } + else + { + $dirs_changed[$i] =~ s#^(.+)[/\\]$#$1#; + } + } + +# Figure out what files have changed using svnlook. +my @files_changed; +foreach my $line (&read_from_process($svnlook, 'changed', $repos, '-t', $txn)) + { + # Split the line up into the modification code and path, ignoring + # property modifications. + if ($line =~ /^.. (.*)$/) + { + push(@files_changed, $1); + } + } + +# Create the list of all modified paths. +my @changed = (@dirs_changed, @files_changed); + +# There should always be at least one changed path. If there are +# none, then there maybe something fishy going on, so just exit now +# indicating that the commit should not proceed. +unless (@changed) + { + die "$0: no changed paths found in txn `$txn'.\n"; + } + +###################################################################### +# Populate the permissions table. + +# Set a hash keeping track of the access rights to each path. Because +# this is an access control script, set the default permissions to +# read-only. +my %permissions; +foreach my $path (@changed) + { + $permissions{$path} = ACCESS_READ_ONLY; + } + +foreach my $section (@sections) + { + # Decide if this section should be used. It should be used if + # there are no users listed at all for this section, or if there + # are users listed and the author is one of them. + my $use_this_section; + + # If there are any users listed, then check if the author of this + # commit is listed in the list. If not, then delete the section, + # because it won't apply. + # + # The configuration file can list users like this on multiple + # lines: + # users = joe@mysite.com betty@mysite.com + # users = bob@yoursite.com + + # Because of the way Config::IniFiles works, check if there are + # any users at all with the scalar return from val() and if there, + # then get the array value to get all users. + my $users = $cfg->val($section, 'users'); + if (defined $users and length $users) + { + my $match_user = 0; + foreach my $entry ($cfg->val($section, 'users')) + { + unless ($match_user) + { + foreach my $user (split(' ', $entry)) + { + if ($author eq $user) + { + $match_user = 1; + last; + } + } + } + } + + $use_this_section = $match_user; + } + else + { + $use_this_section = 1; + } + + next unless $use_this_section; + + # Go through each modified path and match it to the regular + # expression and set the access right if the regular expression + # matches. + my $access = $cfg->val($section, 'access'); + my $match_re = $cfg->val($section, 'match_re'); + foreach my $path (@changed) + { + $permissions{$path} = $access if $path =~ $match_re; + } + } + +# Go through all the modified paths and see if any permissions are +# read-only. If so, then fail the commit. +my @failed_paths; +foreach my $path (@changed) + { + if ($permissions{$path} ne ACCESS_READ_WRITE) + { + push(@failed_paths, $path); + } + } + +if (@failed_paths) + { + warn "$0: user `$author' does not have permission to commit to ", + @failed_paths > 1 ? "these paths:\n " : "this path:\n ", + join("\n ", @failed_paths), "\n"; + exit 1; + } +else + { + exit 0; + } + +sub usage +{ + warn "@_\n" if @_; + die "usage: $0 REPOS TXN-NAME CONF_FILE\n"; +} + +sub safe_read_from_pipe +{ + unless (@_) + { + croak "$0: safe_read_from_pipe passed no arguments.\n"; + } + print "Running @_\n"; + my $pid = open(SAFE_READ, '-|'); + unless (defined $pid) + { + die "$0: cannot fork: $!\n"; + } + unless ($pid) + { + open(STDERR, ">&STDOUT") + or die "$0: cannot dup STDOUT: $!\n"; + exec(@_) + or die "$0: cannot exec `@_': $!\n"; + } + my @output; + while () + { + chomp; + push(@output, $_); + } + close(SAFE_READ); + my $result = $?; + my $exit = $result >> 8; + my $signal = $result & 127; + my $cd = $result & 128 ? "with core dump" : ""; + if ($signal or $cd) + { + warn "$0: pipe from `@_' failed $cd: exit=$exit signal=$signal\n"; + } + if (wantarray) + { + return ($result, @output); + } + else + { + return $result; + } +} + +sub read_from_process + { + unless (@_) + { + croak "$0: read_from_process passed no arguments.\n"; + } + my ($status, @output) = &safe_read_from_pipe(@_); + if ($status) + { + if (@output) + { + die "$0: `@_' failed with this output:\n", join("\n", @output), "\n"; + } + else + { + die "$0: `@_' failed with no output.\n"; + } + } + else + { + return @output; + } +} diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 0000000..9e1de9c --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,69 @@ +#!/bin/sh + +# PRE-COMMIT HOOK +# +# The pre-commit hook is invoked before a Subversion txn is +# committed. Subversion runs this hook by invoking a program +# (script, executable, binary, etc.) named 'pre-commit' (for which +# this file is a template), with the following ordered arguments: +# +# [1] REPOS-PATH (the path to this repository) +# [2] TXN-NAME (the name of the txn about to be committed) +# +# The default working directory for the invocation is undefined, so +# the program should set one explicitly if it cares. +# +# If the hook program exits with success, the txn is committed; but +# if it exits with failure (non-zero), the txn is aborted, no commit +# takes place, and STDERR is returned to the client. The hook +# program can use the 'svnlook' utility to help it examine the txn. +# +# On a Unix system, the normal procedure is to have 'pre-commit' +# invoke other programs to do the real work, though it may do the +# work itself too. +# +# *** NOTE: THE HOOK PROGRAM MUST NOT MODIFY THE TXN, EXCEPT *** +# *** FOR REVISION PROPERTIES (like svn:log or svn:author). *** +# +# This is why we recommend using the read-only 'svnlook' utility. +# In the future, Subversion may enforce the rule that pre-commit +# hooks should not modify the versioned data in txns, or else come +# up with a mechanism to make it safe to do so (by informing the +# committing client of the changes). However, right now neither +# mechanism is implemented, so hook writers just have to be careful. +# +# Note that 'pre-commit' must be executable by the user(s) who will +# invoke it (typically the user httpd runs as), and that user must +# have filesystem-level permission to access the repository. +# +# On a Windows system, you should name the hook program +# 'pre-commit.bat' or 'pre-commit.exe', +# but the basic idea is the same. +# +# The hook program typically does not inherit the environment of +# its parent process. For example, a common problem is for the +# PATH environment variable to not be set to its usual value, so +# that subprograms fail to launch unless invoked via absolute path. +# If you're having unexpected problems with a hook program, the +# culprit may be unusual (or missing) environment variables. +# +# Here is an example hook script, for a Unix /bin/sh interpreter. +# For more examples and pre-written hooks, see those in +# the Subversion repository at +# http://svn.collab.net/repos/svn/trunk/tools/hook-scripts/ and +# http://svn.collab.net/repos/svn/trunk/contrib/hook-scripts/ + +REPOS="$1" +TXN="$2" + +# Make sure that the log message contains some text. +#SVNLOOK=/usr/bin/svnlook +#$SVNLOOK log -t "$TXN" "$REPOS" | \ +# grep "[a-zA-Z0-9]" > /dev/null || exit 1 + +# Check that the author of this commit has the rights to perform +# the commit on the files and directories being modified. +/svn/hooks/commit-access-control.pl "$REPOS" "$TXN" /svn/hooks/commit-access-control.cfg || exit 1 + +# All checks passed, so allow the commit. +exit 0 -- 2.43.0