diff --git a/doc/big-config.mkd b/doc/big-config.mkd index 962b548..48259d2 100644 --- a/doc/big-config.mkd +++ b/doc/big-config.mkd @@ -308,16 +308,7 @@ this (note the clever date command that always gets you last months log file!) ### what are the downsides? -There are some downsides. The first one applies in all cases: - - * If you use the delegation feature, you can no longer define or extend - @groups in a fragment, for security reasons. It will also not let you use - any group other than the @fragname itself (specifically, groups which - contained a subset of the allowed @fragname, which would work normally, do - not work now). - - (If you didn't understand all that, you're probably not using delegation, - so feel free to ignore it!) +There are some downsides. The following apply if individual ("split") conf files are written, which in turn only happens if you used repo names instead of group names on the `repo` diff --git a/doc/delegation.mkd b/doc/delegation.mkd index fd8b4e0..11ba712 100644 --- a/doc/delegation.mkd +++ b/doc/delegation.mkd @@ -1,16 +1,14 @@ ## delegating access control responsibilities -[Thanks to jeromeag for forcing me to think through this...] - ----- - In this document: * lots of repos, lots of users - * splitting up the set of repos into groups - * delegating ownership of groups of repos - * other notes - * security/philosophy note + * how to use delegation + * the subconf command + * backward compatibility + * security notes + * group names + * delegating pubkeys ---- @@ -36,96 +34,118 @@ repos, otherwise it doesn't scale. It would also be nice if we could prevent an admin from creating access rules for *any* repo in the system -- i.e., set limits on what repos he can control. This would be a nice "security" feature. -Delegation offers a way to do all that. Note that delegated admins cannot -create or remove users, nor can they define new repos. They can only define -access control rules for a set of repos they have been given authority for. +Delegation offers a way to do all that. You can allow access control rules +for a set of repos to be specified in a **subconf** file and allow someone (a +**sub-admin**) to make changes within that file. (Note: sub-admins cannot +create or remove users). ----- + -It's easier to show how it all works with an example instead of long -descriptions. +### how to use delegation - +First, you group your repos however you want. In the example below, I'm +considering firefox and lynx (projects at the root of the gitolite server) as +well as *any* repo inside the `browsers` subdirectory, as members of the +`webbrowsers` group. Similarly for the others. -### splitting up the set of repos into groups + @webbrowsers = firefox lynx browsers/..* + @webservers = apache nginx servers/..* + @malwares = conficker storm ms/..* + # side note: if anyone objects, we claim ms stands for "metasploit" ;-) -To start with, recall that gitolite allows you to specify **groups** (of users -or repos, same syntax). So the basic idea is that the main config file -(`conf/gitolite.conf` in your admin repo clone) will specify some repo groups: +Each of these groups is called a **subconf** from here on. - # group your projects/repos however you want - @webbrowser_repos = firefox lynx - @webserver_repos = apache nginx - @malware_repos = conficker storm +Then you designate a **sub-admin** to manage each subconf, and you ensure +(using the standard gitolite feature of restricting pushes by names of changed +files) that a sub-admin can make changes only to her subconf file and nothing +else. - # any other config as usual, including access control lines for any of the - # above projects or groups - - - -### delegating ownership of groups of repos - -Once the repos are grouped, give each person charge of one or more groups. -For example, Alice may be in charge of all web browser development projects, -Bob takes care of web servers, and Mallory, as [tradition][abe] dictates, is -in charge of malware ;-) +For example, Alice is in charge of all web browser development projects. +Similarly, Bob takes care of web servers, and Mallory, as [tradition][abe] +dictates, is in charge of malware ;-) [abe]: http://en.wikipedia.org/wiki/Alice_and_Bob#List_of_characters -You do this by adding files with specific names to the `gitolite-admin` repo: - # the admin repo access was probably like this to start with: repo gitolite-admin - RW+ = sitaram + RW+ = sitaram # now add these lines to the config for the admin repo - RW = alice bob mallory - RW+ NAME/ = sitaram - RW NAME/conf/fragments/webbrowser_repos = alice - RW NAME/conf/fragments/webserver_repos = bob - RW NAME/conf/fragments/malware_repos = mallory + RW = alice bob mallory + RW+ NAME/ = sitaram + RW NAME/conf/subs/webbrowsers = alice + RW NAME/conf/subs/webservers = bob + RW NAME/conf/subs/malwares = mallory -This uses gitolite's ability to restrict pushes by file/dir name being changed --- the syntax you see above ensures that, while "sitaram" does not have any -NAME based restrictions, the other 3 users do. See `conf/example.conf` for -syntax and notes. +Finally, you tell gitolite to pull in these files using the "subconf" command -As you can see, **for each repo group** you want to delegate authority over, -there's a rule for a **corresponding file** in `conf/fragments` in the -`gitolite-admin` repo. If you have write access to that file, you are allowed -to define rules for repos in that repo group. + subconf "subs/*.conf" -In other words, we use gitolite's file/dir NAME-based permissions to "enforce" -the separation between the delegated configs! +You can put this command anywhere in the main gitolite.conf file, but it's +best to put it at the end. -Here's how to use this in practice: +Now alice can clone the admin repo, add a file called `conf/subs/webbrowsers` +with whatever access rules she wants for the repositories under her control, +commit and push. - * Alice clones the `gitolite-admin` repo, and adds a file called - `conf/fragments/webbrowser_repos.conf` +And that's really all there is to it. - * she writes in this file any access control rules for the "firefox" and - "lynx" repos. She should not write access rules for any other project -- - they will be ignored + - * Alice then commits and pushes to the `gitolite-admin` repo +#### the subconf command -Naturally, a successful push invokes the post-update hook that the admin repo -has, which eventually runs the compile script. The **net effect** is as if -you appended the contents of all the "fragment" files, in alphabetical order, -to the bottom of the main file. +This command is much like the "include" command, but in addition it checks +that a subconf does not contain ACL rules for repos that are outside its +purview. - +In the above example, the `webbrowsers` subconf file can only have access +control lines for firefox, lynx, and anything under "browsers/" because those +are the elements of the `@webbrowsers` group. (This is checked using a regex +match, which is why "anything under browsers/" is written `browsers/..*`) -### other notes +In more precise terms: -If you're in big-config mode (`GL_BIG_CONFIG` set to 1 in the rc file), a -fragment file cannot define any new groups; all groups have to be defined in -the main config file or in a file included from it. + * the subconf name is simply the basename of the conf file, without the + .conf extension, and + * the elements of an `@` group of the same name are then used to limit what + repos the subconf can have ACL lines for. ----- +(Additional notes: it can also contain lines for an actual repo called +`webbrowsers`, or, in big-config mode, for a group called `@webbrowsers`). - + -### security/philosophy note +#### backward compatibility + +For backward compatibility, if no `subconf` commands have been seen at the end +of processing the main config file, gitolite pretends you appended + + subconf "conf/fragments/*.conf" + +to the end of the file. + + + +### security notes + + + +#### group names + +You can use "@group"s defined in the main config file but do not attempt to +redefine or extend them in your own subconf file. If you must extend a group +(say `@foo`) defined in the main config file, do this: + + @myfoo = @foo + # now do whatever you want with @myfoo + +Group names you define in your subconf will not clash even if the exact same +name is used in another subconf file, so you need not worry about that. + + + +#### delegating pubkeys + +Short answer: not gonna happen. The delegation feature is meant only for access control rules, not pubkeys. Adding/removing pubkeys is a much more significant event than changing branch @@ -134,12 +154,11 @@ be allowed to do it. Gitolite's "userids" all live in the same namespace. This is unlikely to change, so please don't ask -- it gets real complicated to do otherwise. -Allowing delegated admins to add users means username collisions, which also -means security problems (admin-A creates a pubkey for Admin-B, thus gaining -access to all of Admin-B's stuff). +Allowing sub-admins to add users means username collisions, which also means +security problems (admin-A creates a pubkey for Admin-B, thus gaining access +to all of Admin-B's stuff). If you feel the need to delegate even that, please just go the whole hog and give them separate gitolite instances! It's pretty easy to setup the -*software* itself system-wide, so that many users can use it without all the -"easy install" fuss. See the "system install / user setup" section in -doc/1-INSTALL.mkd for details. +*software* itself system-wide, so that many users can use it; see the root +install method in the install document. diff --git a/doc/gitolite.conf.mkd b/doc/gitolite.conf.mkd index 87e1ff7..d495484 100644 --- a/doc/gitolite.conf.mkd +++ b/doc/gitolite.conf.mkd @@ -80,6 +80,11 @@ config file exists. Files that have been already processed once are skipped, with a warning. +Advanced users: `subconf`, a command that is very closely +related to `include`, is documented [here][subconf]. + +[subconf]: http://sitaramc.github.com/gitolite/doc/delegation.html#_the_subconf_command + ### basic access control diff --git a/src/gl-compile-conf b/src/gl-compile-conf index fe24738..882eedd 100755 --- a/src/gl-compile-conf +++ b/src/gl-compile-conf @@ -68,6 +68,9 @@ my %user_list = (); my %desc = (); my %owner = (); +# backward compat for delegation +my $subconf_seen = 0; + # ---------------------------------------------------------------------------- # subroutines # ---------------------------------------------------------------------------- @@ -104,9 +107,11 @@ sub device_inode { # ---------------------------------------------------------------------------- # detect recursion in include files; see processing of "include" statement later -our %included; +my %included; $included{device_inode("conf/gitolite.conf")}++; +my %prefixed_groupname = (); + sub check_fragment_repo_disallowed { # trying to set access for $repo (='foo')... @@ -133,8 +138,6 @@ sub parse_conf_line # user or repo groups if ($line =~ /^(@\S+) = ?(.*)/) { - die "$ABRT defining groups is not allowed inside fragments\n" - if $GL_BIG_CONFIG and $fragment ne 'master'; # store the members of each group as hash key. Keep track of when # the group was *first* created by using $fragment as the *value* do { $groups{$1}{$_} ||= $fragment } for ( expand_list( split(' ', $2) ) ); @@ -193,6 +196,8 @@ sub parse_conf_line # trying to set access for $repo (='foo')... if (check_fragment_repo_disallowed( $fragment, $repo )) { + my $repo = $repo; + $repo =~ s/^\@$fragment\./locally modified \@/; $ignored_p->{$fragment}{$repo} = 1; next; } @@ -252,17 +257,30 @@ sub parse_conf_line $repos{$repo}{HAS_CONFIG} = 1; } } - # include - elsif ($line =~ /^include "(.+)"/) + # include and subconf. subconf is just a special case of "include", + # saying that the config parse should "switch" contexts + elsif ($line =~ /^(include|subconf) "(.+)"/) { - my $include_glob = $1; + my $include_glob = $2; + my $subconf = ( $1 eq 'subconf' ); + die "$ABRT subconf $fragment attempting to run 'subconf'\n" if $subconf and $fragment ne 'master'; + + # substitute HOSTNAME word if GL_HOSTNAME defined, otherwise leave as is + $include_glob =~ s/\bHOSTNAME\b/$GL_HOSTNAME/ if $GL_HOSTNAME; + for my $file (glob($include_glob =~ m(^/) ? $include_glob : "conf/$include_glob")) { - die "$ABRT included file not found: '$file'\n" unless -f $file; + warn("$WARN included file not found: '$file'\n"), next unless -f $file; my $file_id = device_inode($file); warn("$WARN $file already included\n"), next if ($included{$file_id}++); - parse_conf_file( $file, $fragment ); + if ($subconf) { + die "$ABRT subconf filename should end in .conf\n" unless $file =~ /^.*\/(.*).conf$/; + parse_conf_file( $file, $1 ); + $subconf_seen++; + } else { + parse_conf_file( $file, $fragment ); + } } } # very simple syntax for the gitweb description of repo; one of: @@ -318,8 +336,33 @@ sub parse_conf_file # skip blank lines next unless $line =~ /\S/; + # this is how we prevent subconf hacking; we internally prefix all + # group names *defined* in the subconf (also if they are later used) + # with the subconf name. + + # rules for prefixing the subconf name: prefix it if the @group name + # has appeared earlier in this file on the *left side*. Prefix all + # left side @group names regardless. + + if ($fragment ne 'master') { + my $lhs = ''; + # save 'foo' if it's an '@foo = list' line + $lhs = $1 if $line =~ /^@(\S+) = /; + # prefix all @group in the line + $line =~ s/(^| )(@\S+)(?= |$)/ $1 . ($prefixed_groupname{$fragment}{$2} || $2) /ge; + # now prefix the LHS and store it if needed + if ($lhs) { + $line =~ s/^@\S+ = /"\@$fragment.$lhs = "/e; + $prefixed_groupname{$fragment}{"\@$lhs"} = "\@$fragment.$lhs"; + } + } + parse_conf_line( $line, $fragment, \@repos, \%ignored ); } + # backward compat for delegation + parse_conf_line( 'subconf "fragments/*.conf"', $fragment, \@repos, \%ignored ) + if ($conffile eq $GL_CONF and $fragment eq 'master' and not $subconf_seen); + for my $ig (sort keys %ignored) { warn "\n\t\t***** WARNING *****\n" . @@ -331,33 +374,6 @@ sub parse_conf_file # parse the main config file parse_conf_file($GL_CONF, 'master'); -# parse any delegated fragments -wrap_chdir($GL_ADMINDIR); -for my $fragment_file (glob("conf/fragments/*.conf")) -{ - # we already check (elsewhere) that a fragment called "foo" will not try - # to specify access control for a repo whose name is not "foo" or is not - # part of a group called "foo" created by master - - # meanwhile, I found a possible attack where the admin for group B creates - # a "convenience" group of (a subset of) his users, and then the admin for - # repo group A (alphabetically before B) adds himself to that same group - # in his own fragment. - - # as a result, admin_A now has access to group B repos :( - - # so now we lock the groups hash to the value it had after parsing - # "master", and localise any changes to it by this fragment so that they - # don't propagate to the next fragment. Thus, each fragment now has only - # those groups that are defined in "master" and itself - - local %groups = %groups; - - my $fragment = $fragment_file; - $fragment =~ s/^conf\/fragments\/(.*).conf$/$1/; - parse_conf_file($fragment_file, $fragment); -} - # ---------------------------------------------------------------------------- # (that ends the config file compiler, though we postpone the writing # for now to deal with the latest GL_BIG_CONFIG innovation!) diff --git a/t/t05a-delegation b/t/t05a-delegation index ba9ecef..a6c2281 100644 --- a/t/t05a-delegation +++ b/t/t05a-delegation @@ -87,7 +87,7 @@ echo " " > conf/fragments/u3r.conf ugc u3 < /dev/null [[ $1 == 0 ]] && expect "u3r.conf attempting to set access for r2b" -[[ $1 == 1 ]] && expect "defining groups is not allowed inside fragments" +[[ $1 == 1 ]] && expect "u3r.conf attempting to set access for locally modified @u3r" [[ $1 == 1 ]] && notexpect "u3r.conf attempting to set access for r2b" [[ $1 == 0 ]] && notexpect "defining groups is not allowed inside fragments" git reset --hard origin/master &>/dev/null diff --git a/t/t05b-delegation-wild b/t/t05b-delegation-wild index cd37b92..1d0209b 100644 --- a/t/t05b-delegation-wild +++ b/t/t05b-delegation-wild @@ -108,7 +108,7 @@ echo " " > conf/fragments/u3r.conf ugc u3 < /dev/null [[ $1 == 0 ]] && expect "u3r.conf attempting to set access for r2b" -[[ $1 == 1 ]] && expect "defining groups is not allowed inside fragments" +[[ $1 == 1 ]] && expect "u3r.conf attempting to set access for locally modified @u3r" [[ $1 == 1 ]] && notexpect "u3r.conf attempting to set access for r2b" [[ $1 == 0 ]] && notexpect "defining groups is not allowed inside fragments" git reset --hard origin/master &>/dev/null