new 'subconf' feature to explicitly do delegation

(includes HOSTNAME substitution feature also...)
g2
Sitaram Chamarty 2011-08-27 21:29:02 +05:30
parent 0ec3d77761
commit e139be927a
6 changed files with 155 additions and 124 deletions

View File

@ -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`

View File

@ -1,16 +1,14 @@
## delegating access control responsibilities
[Thanks to jeromeag for forcing me to think through this...]
----
In this document:
* <a href="#_lots_of_repos_lots_of_users">lots of repos, lots of users</a>
* <a href="#_splitting_up_the_set_of_repos_into_groups">splitting up the set of repos into groups</a>
* <a href="#_delegating_ownership_of_groups_of_repos">delegating ownership of groups of repos</a>
* <a href="#_other_notes">other notes</a>
* <a href="#_security_philosophy_note">security/philosophy note</a>
* <a href="#_how_to_use_delegation">how to use delegation</a>
* <a href="#_the_subconf_command">the subconf command</a>
* <a href="#_backward_compatibility">backward compatibility</a>
* <a href="#_security_notes">security notes</a>
* <a href="#_group_names">group names</a>
* <a href="#_delegating_pubkeys">delegating pubkeys</a>
----
@ -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).
----
<a name="_how_to_use_delegation"></a>
It's easier to show how it all works with an example instead of long
descriptions.
### how to use delegation
<a name="_splitting_up_the_set_of_repos_into_groups"></a>
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
<a name="_delegating_ownership_of_groups_of_repos"></a>
### 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
<a name="_the_subconf_command"></a>
* 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.
<a name="_other_notes"></a>
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`).
<a name="_security_philosophy_note"></a>
<a name="_backward_compatibility"></a>
### 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.
<a name="_security_notes"></a>
### security notes
<a name="_group_names"></a>
#### 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.
<a name="_delegating_pubkeys"></a>
#### 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.

View File

@ -80,6 +80,11 @@ config file exists.
Files that have been already processed once are skipped, with a warning.
<font color="gray">Advanced users: `subconf`, a command that is very closely
related to `include`, is documented [here][subconf].</gray>
[subconf]: http://sitaramc.github.com/gitolite/doc/delegation.html#_the_subconf_command
<a name="_basic_access_control"></a>
### basic access control

View File

@ -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!)

View File

@ -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

View File

@ -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