[PATCH RESEND] hgweb: add group authorization
Wagner Bruna
wagner.bruna+mercurial at gmail.com
Tue Feb 26 22:42:08 UTC 2013
Em 07-02-2013 15:36, Markus Zapke-Gründemann escreveu:
> # HG changeset patch
> # User Markus Zapke-Gründemann <markus at keimlink.de>
> # Date 1360231888 -3600
> # Node ID d2dbfdee987a51efb6f4ad69e3b116aa22553326
> # Parent 2fefd1170bf269e26bb304553009f38e0117c342
> hgweb: add group authorization.
(sorry for taking so long to reply, I was kind of internet-impaired these days)
Very useful feature IMHO. I have a few suggestions below:
> diff --git a/mercurial/help/config.txt b/mercurial/help/config.txt
> --- a/mercurial/help/config.txt
> +++ b/mercurial/help/config.txt
> @@ -1286,8 +1286,12 @@ The full set of options is:
> push is not allowed. If the special value ``*``, any remote user can
> push, including unauthenticated users. Otherwise, the remote user
> must have been authenticated, and the authenticated user name must
> - be present in this list. The contents of the allow_push list are
> - examined after the deny_push list.
> + be present in this list. It is also possible to use groups in this
> + list. A group name is prefixed by an ``@``. Groups can either be
> + groups defined in the ``groups_section`` or Unix groups. If a group
> + from the ``groups_section`` has the same name as an Unix group it
> + is used instead. The contents of the allow_push list are examined
> + after the deny_push list.
>
> ``allow_read``
> If the user has not already been denied repository access due to
> @@ -1297,8 +1301,12 @@ The full set of options is:
> denied for the user. If the list is empty or not set, then access
> is permitted to all users by default. Setting allow_read to the
> special value ``*`` is equivalent to it not being set (i.e. access
> - is permitted to all users). The contents of the allow_read list are
> - examined after the deny_read list.
> + is permitted to all users). It is also possible to use groups in
> + this list. A group name is prefixed by an ``@``. Groups can either
> + be groups defined in the ``groups_section`` or Unix groups. If a
> + group from the ``groups_section`` has the same name as an Unix group
> + it is used instead. The contents of the allow_read list are examined
> + after the deny_read list.
>
> ``allowzip``
> (DEPRECATED) Whether to allow .zip downloading of repository
> @@ -1366,8 +1374,13 @@ The full set of options is:
> Whether to deny pushing to the repository. If empty or not set,
> push is not denied. If the special value ``*``, all remote users are
> denied push. Otherwise, unauthenticated users are all denied, and
> - any authenticated user name present in this list is also denied. The
> - contents of the deny_push list are examined before the allow_push list.
> + any authenticated user name present in this list is also denied. It
> + is also possible to use groups in this list. A group name is
> + prefixed by an ``@``. Groups can either be groups defined in the
> + ``groups_section`` or Unix groups. If a group from the
> + ``groups_section`` has the same name as an Unix group it is used
> + instead. The contents of the deny_push list are examined before the
> + allow_push list.
>
> ``deny_read``
> Whether to deny reading/viewing of the repository. If this list is
> @@ -1380,9 +1393,12 @@ The full set of options is:
> deny_read and allow_read are empty or not set, then access is
> permitted to all users by default. If the repository is being
> served via hgwebdir, denied users will not be able to see it in
> - the list of repositories. The contents of the deny_read list have
> - priority over (are examined before) the contents of the allow_read
> - list.
> + the list of repositories. It is also possible to use groups in this
> + list. A group name is prefixed by an ``@``. Groups can either be
> + groups defined in the ``groups_section`` or Unix groups. If a group
> + from the ``groups_section`` has the same name as an Unix group it is
> + used instead. The contents of the deny_read list have priority over
> + (are examined before) the contents of the allow_read list.
>
> ``descend``
> hgwebdir indexes will not descend into subdirectories. Only repositories
> @@ -1400,6 +1416,30 @@ The full set of options is:
> ``errorlog``
> Where to output the error log. Default is stderr.
>
> +``groups_section``
> + Name of hgrc section used to define groups for authorization.
> + Default is ``web.groups``. Use the section to define the groups used
> + by authorization.
> +
> + Example::
> +
> + [web]
> + allow_read = @devs
> +
> + [web.groups]
Minor nit: the dot could cause some confusion with the section.key notation,
so I'd call it simply "webgroups" (or even "usergroups").
> + devs = alice, bob, clara, david
> +
> + Groups can contain other groups::
> +
> + [web]
> + allow_read = @devs, @testers
> + allow_push = @devs
> +
> + [web.groups]
> + devs = alice, bob, clara, david
> + ci = hudson
> + testers = @ci, lisa, mario
What about a different notation, similar to the [auth] section, supporting
per-group attributes:
[webgroups]
devs.type = hgconfig
devs.members = alice, bob, clara, david
# this would pull members from the same named unix group...
anotherteam.type = unix
# ...or perhaps explicitly:
# anotherteam.group = anotherteamunixgroup
# the '@' marker would be fully optional then:
@ci.members = hudson
# @ci.type = hgconfig would be the default here
Together with a bit more flexibility in the _ismember function below (that
could be implemented in a follow-up patch), additional group providers could
then be easily added through extensions (ldap comes to mind).
Regards,
Wagner
> +
> ``guessmime``
> Control MIME types for raw download of file content.
> Set to True to let hgweb guess the content type from the file
> diff --git a/mercurial/hgweb/common.py b/mercurial/hgweb/common.py
> --- a/mercurial/hgweb/common.py
> +++ b/mercurial/hgweb/common.py
> @@ -8,6 +8,8 @@
>
> import errno, mimetypes, os
>
> +from mercurial import util
> +
> HTTP_OK = 200
> HTTP_NOT_MODIFIED = 304
> HTTP_BAD_REQUEST = 400
> @@ -18,6 +20,53 @@ HTTP_METHOD_NOT_ALLOWED = 405
> HTTP_SERVER_ERROR = 500
>
>
> +def _get_users(ui, group, seen=None):
> + """Return the users of the group as list."""
> + # update list of groups seen so far for detecting recursions
> + if not seen:
> + seen = []
> + seen.append(group)
> + # check which section to use to lookup groups
> + section = ui.config('web', 'groups_section', 'web.groups')
> + # first, try to use group definition from groups_section
> + users = []
> + hgrcusers = ui.configlist(section, group)
> + if hgrcusers:
> + for item in hgrcusers:
> + if not item.startswith('@'):
> + users.append(item)
> + continue
> + if item[1:] in seen:
> + raise ErrorResponse(HTTP_UNAUTHORIZED,
> + 'recursion detected for group "%s" in group "%s"' %
> + (item[1:], group))
> + users += _get_users(ui, item[1:], seen)
> + if not users:
> + # if no users found in group definition, get users from OS-level group
> + try:
> + users = util.groupmembers(group)
> + except KeyError:
> + raise ErrorResponse(HTTP_UNAUTHORIZED,
> + 'group "%s" is undefined' % group)
> + return users
> +
> +
> +def _is_member(ui, user, group):
> + """Check recursively if a user is member of a group.
> +
> + If the group equals * all users are members.
> + """
> + if group == ['*'] or user in group:
> + return True
> + for item in group:
> + if not item.startswith('@'):
> + continue
> + users = _get_users(ui, item[1:])
> + if user in users:
> + return True
> + return False
> +
> +
> def checkauthz(hgweb, req, op):
> '''Check permission for operation based on request data (including
> authentication info). Return if op allowed, else raise an ErrorResponse
> @@ -25,18 +74,19 @@ def checkauthz(hgweb, req, op):
>
> user = req.env.get('REMOTE_USER')
>
> + # check read permission
> deny_read = hgweb.configlist('web', 'deny_read')
> - if deny_read and (not user or deny_read == ['*'] or user in deny_read):
> + if deny_read and (not user or _is_member(hgweb.repo.ui, user, deny_read)):
> raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
>
> allow_read = hgweb.configlist('web', 'allow_read')
> - result = (not allow_read) or (allow_read == ['*'])
> - if not (result or user in allow_read):
> + if not (not allow_read or _is_member(hgweb.repo.ui, user, allow_read)):
> raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
>
> + # check pull permission
> if op == 'pull' and not hgweb.allowpull:
> raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
> - elif op == 'pull' or op is None: # op is None for interface requests
> + elif op == 'pull' or op is None: # op is None for interface requests
> return
>
> # enforce that you can only push using POST requests
> @@ -50,12 +100,13 @@ def checkauthz(hgweb, req, op):
> if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
> raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
>
> + # check push permission
> deny = hgweb.configlist('web', 'deny_push')
> - if deny and (not user or deny == ['*'] or user in deny):
> + if deny and (not user or _is_member(hgweb.repo.ui, user, deny)):
> raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
>
> allow = hgweb.configlist('web', 'allow_push')
> - result = allow and (allow == ['*'] or user in allow)
> + result = allow and _is_member(hgweb.repo.ui, user, allow)
> if not result:
> raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
>
> diff --git a/mercurial/hgweb/hgwebdir_mod.py b/mercurial/hgweb/hgwebdir_mod.py
> --- a/mercurial/hgweb/hgwebdir_mod.py
> +++ b/mercurial/hgweb/hgwebdir_mod.py
> @@ -10,8 +10,8 @@ import os, re, time
> from mercurial.i18n import _
> from mercurial import ui, hg, scmutil, util, templater
> from mercurial import error, encoding
> -from common import ErrorResponse, get_mtime, staticfile, paritygen, \
> - get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
> +from common import _is_member, ErrorResponse, get_mtime, staticfile, \
> + paritygen, get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
> from hgweb_mod import hgweb, makebreadcrumb
> from request import wsgirequest
> import webutil
> @@ -164,12 +164,12 @@ class hgwebdir(object):
> user = req.env.get('REMOTE_USER')
>
> deny_read = ui.configlist('web', 'deny_read', untrusted=True)
> - if deny_read and (not user or deny_read == ['*'] or user in deny_read):
> + if deny_read and (not user or _is_member(ui, user, deny_read)):
> return False
>
> allow_read = ui.configlist('web', 'allow_read', untrusted=True)
> # by default, allow reading if no allow_read option has been set
> - if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
> + if (not allow_read) or _is_member(ui, user, allow_read):
> return True
>
> return False
> diff --git a/tests/test-hgweb-authz.t b/tests/test-hgweb-authz.t
> new file mode 100644
> --- /dev/null
> +++ b/tests/test-hgweb-authz.t
> @@ -0,0 +1,111 @@
> +This test exercises the authorization functionality with a dummy script
> +
> + $ cat <<EOF > dummywsgi
> + > import os
> + > import sys
> + >
> + > from mercurial.hgweb import hgweb
> + >
> + > app = hgweb(os.path.join(os.environ['TESTTMP'], 'hgweb.config'))
> + > environ = {
> + > 'SCRIPT_NAME': '',
> + > 'REQUEST_METHOD': 'GET',
> + > 'PATH_INFO': sys.argv[1],
> + > 'SERVER_PROTOCOL': 'HTTP/1.0',
> + > 'QUERY_STRING': '',
> + > 'CONTENT_LENGTH': '0',
> + > 'SERVER_NAME': 'localhost',
> + > 'SERVER_PORT': '80',
> + > 'REPO_NAME': sys.argv[1],
> + > 'HTTP_HOST': 'localhost:80',
> + > 'REMOTE_USER': sys.argv[2],
> + > 'wsgi.input': sys.stdin,
> + > 'wsgi.url_scheme': 'http',
> + > 'wsgi.multithread': False,
> + > 'wsgi.version': (1, 0),
> + > 'wsgi.run_once': False,
> + > 'wsgi.errors': sys.stderr,
> + > 'wsgi.multiprocess': False,
> + > }
> + >
> + > def start_response(status, headers, exc_info=None):
> + > def dummy_response(data):
> + > pass
> + > sys.stdout.write(status + '\n')
> + > return dummy_response
> + >
> + > app(environ, start_response)
> + > EOF
> +
> +creating test repository
> +
> + $ hg init r1
> + $ cd r1
> + $ echo c1 > f1
> + $ echo c2 > f2
> + $ hg ci -A -m "init" f1 f2
> +
> +writing hgweb.config
> +
> + $ cd ..
> + $ cat <<EOF > hgweb.config
> + > [paths]
> + > r1 = `pwd`/r1
> + > EOF
> +
> +group authorization test
> +
> + $ cat <<EOF > r1/.hg/hgrc
> + > [web]
> + > allow_read = @developers, cathrin
> + >
> + > [web.groups]
> + > developers = alice, bob
> + > EOF
> +
> + $ python ./dummywsgi r1 alice
> + 200 Script output follows
> + $ python ./dummywsgi r1 bob
> + 200 Script output follows
> + $ python ./dummywsgi r1 cathrin
> + 200 Script output follows
> + $ python ./dummywsgi r1 nosuchuser
> + 401 read not authorized
> +
> +groups can contain other groups
> +
> + $ cat <<EOF > r1/.hg/hgrc
> + > [web]
> + > allow_read = @developers, @testers
> + >
> + > [web.groups]
> + > developers = alice, bob
> + > ci = hudson
> + > testers = @ci, lisa, mario
> + > EOF
> +
> + $ python ./dummywsgi r1 hudson
> + 200 Script output follows
> +
> +using an unknown groups fails
> +
> + $ cat <<EOF > r1/.hg/hgrc
> + > [web]
> + > allow_read = @quux
> + > EOF
> +
> + $ python ./dummywsgi r1 alice
> + 401 group "quux" is undefined
> +
> +using a recursive groups setup is not allowed
> +
> + $ cat <<EOF > r1/.hg/hgrc
> + > [web]
> + > allow_read = @developers
> + >
> + > [web.groups]
> + > developers = alice, bob, @developers
> + > EOF
> +
> + $ python ./dummywsgi r1 alice
> + 401 recursion detected for group "developers" in group "developers"
More information about the Mercurial-devel
mailing list