Skip to content

Instantly share code, notes, and snippets.

@kraftb
Created March 4, 2014 16:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kraftb/9349612 to your computer and use it in GitHub Desktop.
Save kraftb/9349612 to your computer and use it in GitHub Desktop.
An experiment for implementing git branach read access control
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