Created
March 4, 2014 16:19
-
-
Save kraftb/9349612 to your computer and use it in GitHub Desktop.
An experiment for implementing git branach read access control
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/src/VERSION b/src/VERSION | |
new file mode 100644 | |
index 0000000..56fa930 | |
--- /dev/null | |
+++ b/src/VERSION | |
@@ -0,0 +1 @@ | |
+v3.5.3.1-1-gf8776f5 | |
diff --git a/src/gitolite-shell b/src/gitolite-shell | |
index 7a27cc0..f6a3442 100755 | |
--- a/src/gitolite-shell | |
+++ b/src/gitolite-shell | |
@@ -4,6 +4,7 @@ | |
# ---------------------------------------------------------------------- | |
use FindBin; | |
+use IPC::Open2; | |
BEGIN { $ENV{GL_BINDIR} = $FindBin::RealBin; } | |
BEGIN { $ENV{GL_LIBDIR} = "$ENV{GL_BINDIR}/lib"; } | |
@@ -15,6 +16,7 @@ BEGIN { $ENV{HOME} = $ENV{GITOLITE_HTTP_HOME} if $ENV{GITOLITE_HTTP_HOME}; } | |
use Gitolite::Rc; | |
use Gitolite::Common; | |
use Gitolite::Conf::Load; | |
+use Gitolite::Branch; | |
use strict; | |
use warnings; | |
@@ -135,12 +137,14 @@ sub main { | |
_system( "git", "http-backend" ); | |
} else { | |
my $repodir = "'$rc{GL_REPO_BASE}/$repo.git'"; | |
- _system( "git", "shell", "-c", "$verb $repodir" ); | |
+# _system( "git", "shell", "-c", "$verb $repodir" ); | |
+ gl_handle_request( $user, $repo, $verb, $repodir ); | |
} | |
trigger( 'POST_GIT', $repo, $user, $aa, 'any', $verb ); | |
} | |
# ---------------------------------------------------------------------- | |
+ | |
sub parse_soc { | |
my $soc = $ENV{SSH_ORIGINAL_COMMAND}; | |
diff --git a/src/lib/Gitolite/Branch.pm b/src/lib/Gitolite/Branch.pm | |
new file mode 100644 | |
index 0000000..4ef05fc | |
--- /dev/null | |
+++ b/src/lib/Gitolite/Branch.pm | |
@@ -0,0 +1,360 @@ | |
+package Gitolite::Branch; | |
+ | |
+# functions for handling per branch (per refspec) access control | |
+# ---------------------------------------------------------------------- | |
+ | |
+@EXPORT = qw( | |
+ gl_handle_request | |
+); | |
+ | |
+use Exporter 'import'; | |
+ | |
+use Gitolite::Rc; | |
+use Gitolite::Common; | |
+use Gitolite::Conf::Load; | |
+use IPC::Open2; | |
+ | |
+# use strict; | |
+use warnings; | |
+ | |
+# ---------------------------------------------------------------------- | |
+ | |
+# This method is the main handler for requests if branch access control | |
+# is enabled. Instead of calling "git shell" directly it intercepts the | |
+# communication and performs access control. | |
+sub gl_handle_request { | |
+ my ( $user, $repo, $verb, $repodir ) = @_; | |
+ | |
+ local $| = 1; | |
+ | |
+ if ( $verb eq "git-upload-pack" ) { | |
+ my @tmp; | |
+ | |
+ # Start GIT shell and open STDOUT($souce) / STDIN($drain) as IO handles | |
+ open2( my $source, my $drain, "git", "shell", "-c", "$verb $repodir" ) or _die "Couldn't execute git shell"; | |
+ | |
+ #-------------------------------------------------------------------------- | |
+ # PROTOCOL STEP 1 READ: Retrieve all available references from server | |
+ #-------------------------------------------------------------------------- | |
+ my ( $serverCapabilities, $head, %refs ) = gl_reference_discovery__read( $source ); | |
+ print STDERR "server capabilities: \"" . $serverCapabilities . "\"\n"; | |
+ | |
+ | |
+ #-------------------------------------------------------------------------- | |
+ # GITOLITE Feature: Filter out references with no access | |
+ #-------------------------------------------------------------------------- | |
+# DEBUG: Log available references BEFORE access check | |
+# print STDERR "before access check:\n"; | |
+# while ( my ( $hash, $ref ) = each( %refs ) ) { | |
+# print STDERR "$hash :: $ref\n"; | |
+# } | |
+ | |
+ # Filter out allowed branches (references) | |
+ %refs = gl_branch_access( $user, $repo, 'R', %refs ); | |
+ | |
+# DEBUG: Log available references AFTERaccess check | |
+# print STDERR "afteraccess check:\n"; | |
+# while ( my ( $hash, $ref ) = each( %refs ) ) { | |
+# print STDERR "$hash :: $ref\n"; | |
+# } | |
+ | |
+ | |
+ #-------------------------------------------------------------------------- | |
+ # PROTOCOL STEP 1 SEND: Write (filtered) available references to client | |
+ #-------------------------------------------------------------------------- | |
+ # Send the "HEAD" line (+ capabilities) to the client | |
+ gl_reference_discovery__write( STDOUT, $head, $serverCapabilities, %refs ); | |
+ | |
+ | |
+ #-------------------------------------------------------------------------- | |
+ # PROTOCOL STEP 2.1 READ: Retrieve wanted references from client | |
+ #-------------------------------------------------------------------------- | |
+ # Retrieve from the client (STDIN) which object ids (hashes) it would like to have ("want"). | |
+ my @wanted; | |
+ my @have; | |
+ my $clientCapabilities = ""; | |
+ my $doneReceived = 0; | |
+ my $noLines = 0; | |
+ | |
+ ( @wanted, $clientCapabilities, $doneReceived, $noLines ) = gl_packfile_negotiation__read( STDIN , "want" ); | |
+ if ( $noLines ) { | |
+ # If there were no lines during reading "want" the client decided to | |
+ # stop packfile negotiation to tell the server it can gracefully terminate. | |
+ # The client requested to end communication. | |
+ gl_send_flush_pkt( $drain ); | |
+ exit 0; | |
+ } | |
+ print STDERR "client capabilities: \"$clientCapabilities\"\n"; | |
+ | |
+ #-------------------------------------------------------------------------- | |
+ # GITOLITE Feature: Filter out references with no access | |
+ #-------------------------------------------------------------------------- | |
+ my @wanted_ok; | |
+ for ( @wanted ) { | |
+ push @wanted_ok, $_ if exists $refs{$_}; | |
+ } | |
+ | |
+ #-------------------------------------------------------------------------- | |
+ # PROTOCOL STEP 2.1 SEND: Write filtered, wanted object ids to server | |
+ #-------------------------------------------------------------------------- | |
+ gl_packfile_negotiation__writeServer( $drain, @wanted_ok, $clientCapabilities, "want" ); | |
+ | |
+ | |
+ #-------------------------------------------------------------------------- | |
+ # PROTOCOL STEP 2.2 READ: Handle "have" blocks | |
+ #-------------------------------------------------------------------------- | |
+ # Read a block of "have" commands from client. Ends with flush packet. | |
+ # Send the block to the server. Read NAK/ACK reply from server. | |
+ # | |
+ # If the client finished a block with a "done" line there is only one | |
+ # more line from the Server containing a "NAK" or "ACK". | |
+ $doneReceived = 0; | |
+ unless ( $doneReceived ) { | |
+ ( @wanted, $clientCapabilities, $doneReceived, $noLines ) = gl_packfile_negotiation__read( STDIN , "want" ); | |
+ gl_send_pkt_line( $handle , gl_make_line( "done" ) ); | |
+ | |
+ | |
+ | |
+ | |
+ print STDERR "bla"; | |
+ | |
+ } else { | |
+ # Simply call git shell via 'system' for other stuff | |
+ _system( "git", "shell", "-c", "$verb $repodir" ); | |
+ } | |
+} | |
+ | |
+ | |
+############################################### | |
+# Functions for STEP 1: Reference discovery | |
+############################################### | |
+ | |
+# Performs the read operation of "reference discovery" (Step 1) of the GIT protocol. | |
+# It reads the references available being sent by the server. | |
+# | |
+# @see pack-protocol.txt | |
+sub gl_reference_discovery__read { | |
+ my ( $handle ) = @_; | |
+ my $buffer = ""; | |
+ my $skipcap = 0; | |
+ my $head = ""; | |
+ my $capabilities = ""; | |
+ my %refs; | |
+ | |
+ # Read in available references (and eventually preceding "HEAD" reference) | |
+ while ($buffer = gl_read_pkt_line( $handle )) { | |
+# print STDERR "gl_reference_discovery__read: \"$buffer\""; | |
+ my ( $hash, $ref, $cap ) = gl_parse_pkt_line( $buffer ); | |
+ | |
+ # The first received line contains the capabilities (if any) | |
+ $capabilities = $cap unless $skipcap; | |
+ $skipcap = 1; | |
+ | |
+ # If the first received line was the hash of HEAD remember it | |
+ if ( $ref eq 'HEAD' ) { | |
+ $head = $hash; | |
+ } else { | |
+ $refs{$hash} = $ref; | |
+ } | |
+ } | |
+ return ( $capabilities, $head, %refs ); | |
+} | |
+ | |
+# Performs the write operation of "reference discovery" (Step 1) of the GIT protocol. | |
+# It writes the available (filtered) references to the client. | |
+# | |
+# @see pack-protocol.txt | |
+sub gl_reference_discovery__write { | |
+ my ( $handle, $head, $capabilities, %refs ) = @_; | |
+ | |
+ if ( $head ne '' and exists $refs{$head} ) { | |
+ gl_send_pkt_line( $handle, gl_make_line( $head, 'HEAD', $capabilities ) ); | |
+ $capabilities = ""; | |
+ } else { | |
+ _die "gl_reference_discovery__write: Protocol error #1: No valid HEAD. Eventually create a fallback HEAD for filtered out heads"; | |
+ } | |
+ | |
+ # Send the remaining available references to the client | |
+ while ( my ( $hash, $ref ) = each( %refs ) ) { | |
+ gl_send_pkt_line( $handle , gl_make_line( $hash, $ref, $capabilities ) ); | |
+ $capabilities = ""; | |
+ } | |
+ gl_send_flush_pkt( $handle ); | |
+} | |
+ | |
+ | |
+ | |
+############################################### | |
+# Functions for STEP 2: Packfile negotiation | |
+############################################### | |
+ | |
+# Performs part of the read operation of "packfile negotiation" (Step 2) of the GIT protocol. | |
+# It reads the "want" lines from the client. | |
+sub gl_packfile_negotiation__readClient { | |
+ my ( $handle, $type ) = @_; | |
+ my $buffer = ""; | |
+ my $capabilities = ""; | |
+ my $skipcap = 0; | |
+ my $doneReceived = 0; | |
+ my @objectIds; | |
+ | |
+ while ( $buffer = gl_read_pkt_line( $handle ) ) { | |
+ my ( $action, $objId, $cap ) = gl_parse_pkt_line( $buffer ); | |
+ | |
+ # The first received line contains the capabilities (if any) | |
+ $capabilities = $cap unless $skipcap; | |
+ $skipcap = 1; | |
+ | |
+ # Only "want" actions are allowed | |
+ if ( $action eq $type ) { | |
+ push @objectIds, $hash; | |
+ } elsif ( ( $action eq "done" ) && ( $type eq "have" ) ) { | |
+ $doneReceived = 1; | |
+ last; | |
+ } else { | |
+ _die "gl_packfile_negotiation__readClient: Protocol error #1 ($action/$type)"; | |
+ } | |
+ } | |
+ print STDERR "packfile_negotiation__readClient: END\n"; | |
+ | |
+ # | |
+ # If there were no lines during reading "have" this means the client doesn't | |
+ # have anything. | |
+ return ( (), $capabilities, $doneReceived, $ ); | |
+ } | |
+ | |
+ return ( @objectIds, $capabilities, $doneReceived, $skipcap ? 0 : 1); | |
+} | |
+ | |
+# Performs part of the read operation of "packfile negotiation" (Step 2) of the GIT protocol. | |
+# It reads one block of the "have" lines from the client. | |
+# A block of "have" line (max. 32) ends with a flush pkt. | |
+# If the client has finished sending "have" lines it will send "done" after the flush pkt. | |
+sub gl_packfile_negotiation__readHave { | |
+ my ( $handle ) = @_; | |
+ my $buffer = ""; | |
+ my $capabilities = ""; | |
+ my $skipcap = 0; | |
+ my @have; | |
+ | |
+ print STDERR "packfile_negotiation__read: Reading \"have\" lines\n"; | |
+ while ( $buffer = gl_read_pkt_line( $handle ) ) { | |
+ my ( $action, $hash ) = gl_parse_pkt_line( $buffer ); | |
+ | |
+ # Only "have" actions are allowed | |
+ if ( $action eq 'have' ) { | |
+ push @have, $hash; | |
+ } else { | |
+ _die "gl_packfile_negotiation__read: Protocol error #1 ($action)"; | |
+ } | |
+ | |
+ print STDERR "packfile_negotiation__read: \"$buffer\"\n"; | |
+ if ( $buffer =~ /^\s*done\s*$/ ) { | |
+ last; | |
+ } | |
+ } | |
+ | |
+ if ( ( $buffer =~ /^\s*done\s*$/ ) && scalar(@have) ) { | |
+ _die "gl_packfile_negotiation__readHave: Protocol error #1 (" . scalar(@have) . ")"; | |
+ } | |
+ | |
+ return ( @have ); | |
+} | |
+ | |
+ | |
+# Performs the write operation of "packfile negotiation" (Step 2) of the GIT protocol. | |
+# It writes the "want" and "have" lines received from the client (and being filtered) to the server. | |
+sub gl_packfile_negotiation__writeServer { | |
+ my ( $handle, @objectIds, $capabilities, $type ) = @_; | |
+ | |
+ for ( @objectIds ) { | |
+ gl_send_pkt_line( $handle , gl_make_line( $type, $_, $capabilities ) ); | |
+ $capabilities = ""; | |
+ } | |
+ gl_send_flush_pkt( $handle ); | |
+} | |
+ | |
+ | |
+######################################################### | |
+# PKT-LINE FUNCTIONS: These functions handle reading | |
+# and writing packet lines and the flush packet. | |
+######################################################### | |
+ | |
+# Sends a "flush" packet (Characters '0000') | |
+sub gl_send_flush_pkt { | |
+ my ( $handle ) = @_; | |
+ print $handle '0000'; | |
+} | |
+ | |
+# Reads a packet line (PKT-LINE) and returns the content | |
+sub gl_read_pkt_line { | |
+ my ( $handle ) = @_; | |
+ my $buffer = ""; | |
+ _die "gl_read_pkt_line: Protocol error #1" unless read( $handle, $buffer, 4 ) == 4; | |
+ my $bytes = hex( $buffer ); | |
+ $buffer = ""; | |
+ if ( $bytes > 4 ) { | |
+ $bytes -= 4; | |
+ my $received = read( $handle, $buffer, $bytes ); | |
+ _die "gl_read_pkt_line: Protocol error #2 ($received/$bytes)" unless ( $received == $bytes ); | |
+ } elsif ( $bytes > 0 ) { | |
+ _die "gl_read_pkt_line: Protocol error #3 ($bytes)"; | |
+ } | |
+ return $buffer; | |
+} | |
+ | |
+ | |
+# Sends the passed argument as packet line (PKT-LINE) | |
+sub gl_send_pkt_line { | |
+ my ( $handle, $line ) = @_; | |
+ my $len = length( $line ) + 4; | |
+ print $handle (sprintf('%04x', $len) . $line); | |
+} | |
+ | |
+ | |
+ | |
+######################################################### | |
+# PARSERS/GENERATORS: These functions parse and generate | |
+# packet lines from passed arguments | |
+######################################################### | |
+ | |
+# Generates a packet line PKT-LINE from the passed arguments | |
+sub gl_make_line { | |
+ my ( $line, $postSpace, $capabilities ) = @_; | |
+ $line .= ' ' . $postSpace if exists $_[1]; | |
+ $line .= "\0$capabilities" if exists $_[2]; | |
+ return $line; | |
+} | |
+ | |
+# Parses a packet line (PKT-LINE) into pieces. | |
+sub gl_parse_pkt_line { | |
+ my ( $buffer ) = @_; | |
+ $buffer =~ s/\s*(.*)\s*$/$1/g; | |
+ my @parts = split( "\0", $buffer ); | |
+ my $capabilities = $parts[1]; | |
+ $capabilities =~ s/\s*(.*)\s*/$1/g unless scalar(@parts) < 2; | |
+ my ( $preSpace, $postSpace ) = split ( / /, $parts[0] ); | |
+# print STDERR "DEBUG gl_parse_pkt_line: \"$preSpace|$postSpace|$capabilities\"\n"; | |
+ return ( $preSpace, $postSpace, $capabilities ); | |
+} | |
+ | |
+ | |
+ | |
+######################################################### | |
+# ACCESS CONTROL: Perform access control checks | |
+######################################################### | |
+ | |
+# This function filters the passed %refs and only returns refspec's for which | |
+# the passed user/repo/access combination allows access | |
+sub gl_branch_access { | |
+ my ( $user, $repo, $aa, %refs ) = @_; | |
+ my %result; | |
+ while ( my ( $hash, $ref ) = each( %refs ) ) { | |
+ if ( access( $repo, $user, $aa, $ref ) !~ /DENIED/ ) { | |
+ $result{$hash} = $ref; | |
+ } | |
+ } | |
+ return %result; | |
+} | |
+ | |
+ | |
diff --git a/src/lib/Gitolite/Conf/Load.pm b/src/lib/Gitolite/Conf/Load.pm | |
index 295e888..29d67f4 100644 | |
--- a/src/lib/Gitolite/Conf/Load.pm | |
+++ b/src/lib/Gitolite/Conf/Load.pm | |
@@ -104,7 +104,7 @@ sub access { | |
for my $r (@rules) { | |
my $perm = $r->[1]; | |
my $refex = $r->[2]; $refex =~ s(/USER/)(/$user/); | |
- trace( 3, "perm=$perm, refex=$refex" ); | |
+ trace( 3, "perm=$perm, refex=$refex, ref=$ref" ); | |
# skip 'deny' rules if the ref is not (yet) known | |
next if $perm eq '-' and $ref eq 'any' and not $deny_rules; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment