Skip to content

Commit

Permalink
authz: authorization framework
Browse files Browse the repository at this point in the history
Signed-off-by: Pierre Tardy <tardyp@gmail.com>
  • Loading branch information
tardyp authored and Pierre Tardy committed Jul 11, 2015
1 parent be917ec commit cde0c9a
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 0 deletions.
95 changes: 95 additions & 0 deletions master/buildbot/test/unit/test_www_authz.py
@@ -0,0 +1,95 @@
from buildbot.test.util import www
from buildbot.www import authz
from twisted.trial import unittest
from buildbot.test.fake import fakedb
from buildbot.www.authz.roles import RolesFromGroups, RolesFromEmails, RolesFromOwner
from buildbot.www.authz.endpointmatchers import AnyEndpointMatcher
from buildbot.www.authz.endpointmatchers import ForceBuildEndpointMatcher
from buildbot.www.authz.endpointmatchers import BranchEndpointMatcher
from buildbot.www.authz.endpointmatchers import ViewBuildsEndpointMatcher
from buildbot.www.authz.endpointmatchers import StopBuildEndpointMatcher


class Authz(www.WwwTestMixin, unittest.TestCase):
def setUp(self):
authzcfg = authz.Authz(
stringsMatcher=authz.Authz.fnmatchMatcher, # simple matcher with '*' glob character
# stringsMatcher = authz.Authz.reMatcher, # if you prefer regular expressions
allowRules=[
# admins can do anything,
# defaultDeny=False: if user does not have the admin role, we continue parsing rules
AnyEndpointMatcher(role="admins", defaultDeny=False),

# rules for viewing builds, builders, step logs
# depending on the sourcestamp or buildername
ViewBuildsEndpointMatcher(branch="secretbranch", role="agents"),
ViewBuildsEndpointMatcher(project="secretproject", role="agents"),
ViewBuildsEndpointMatcher(branch="*", role="*"),
ViewBuildsEndpointMatcher(project="*", role="*"),

StopBuildEndpointMatcher(role="owner"),

# nine-* groups can do stuff on the nine branch
BranchEndpointMatcher(branch="nine", role="nine-*"),
# eight-* groups can do stuff on the eight branch
BranchEndpointMatcher(branch="eight", role="eight-*"),

# *-try groups can start "try" builds
ForceBuildEndpointMatcher(builder="try", role="*-developers"),
# *-mergers groups can start "merge" builds
ForceBuildEndpointMatcher(builder="merge", role="*-mergers"),
# *-releasers groups can start "release" builds
ForceBuildEndpointMatcher(builder="release", role="*-releasers"),
],
roleMatchers=[
RolesFromGroups(groupPrefix="buildbot-"),
RolesFromEmails(admins=["homer@springfieldplant.com"],
agents=["007@mi6.uk"]),
RolesFromOwner(role="owner")
]
)
self.users = dict(homer=dict(email="homer@springfieldplant.com"),
bond=dict(email="007@mi6.uk"),
nineuser=dict(email="user@nine.com", groups=["buildbot-nine-mergers",
"buildbot-nine-developers"]),
eightuser=dict(email="user@eight.com", groups=["buildbot-eight-deverlopers"])
)
self.master = self.make_master(url='h:/a/b/', authz=authzcfg)
self.authz = self.master.authz
self.master.db.insertTestData([
fakedb.Builder(id=77, name="mybuilder"),
fakedb.Master(id=88),
fakedb.Buildslave(id=13, name='sl'),
fakedb.Buildset(id=8822),
fakedb.BuildsetProperty(buildsetid=8822, property_name='owner',
property_value='["user@nine.com", "force"]'),
fakedb.BuildRequest(id=82, buildsetid=8822),
fakedb.Build(id=13, builderid=77, masterid=88, buildslaveid=13,
buildrequestid=82, number=3),
fakedb.Build(id=14, builderid=77, masterid=88, buildslaveid=13,
buildrequestid=82, number=4),
fakedb.Build(id=15, builderid=77, masterid=88, buildslaveid=13,
buildrequestid=82, number=5),
])

def assertUserAllowed(self, ep, action, user):
ret = self.authz.isUserAllowed(ep.split("/"), action, self.users[user])
self.assertEqual(ret, True)

def assertUserForbidden(self, ep, action, user):
ret = self.authz.isUserAllowed(ep.split("/"), action, self.users[user])
self.assertEqual(ret, False)

def test_anyEndpoint(self):
self.assertUserAllowed("foo/bar", "get", "homer")
self.assertUserForbidden("foo/bar", "get", "bond")

def test_stopBuild(self):
# admin can always stop
self.assertUserAllowed("builds/13", "stop", "homer")
# owner can always stop
self.assertUserAllowed("buildrequests/82", "stop", "nineuser")
self.assertUserAllowed("buildrequests/82", "stop", "nineuser")
# not owner cannot stop
self.assertUserForbidden("buildrequests/82", "stop", "eightuser")
self.assertUserForbidden("buildrequests/82", "stop", "eightuser")
2 changes: 2 additions & 0 deletions master/buildbot/www/authz/__init__.py
@@ -0,0 +1,2 @@
from buildbot.www.authz.authz import Authz
__all__ = ["Authz"]
22 changes: 22 additions & 0 deletions master/buildbot/www/authz/authz.py
@@ -0,0 +1,22 @@
import fnmatch
import re


class Authz(object):

@staticmethod
def fnmatchMatcher(value, match):
return fnmatch.fnmatch(value, match)

@staticmethod
def reMatcher(value, match):
return re.match(match, value)

def __init__(self, allowRules, roleMatchers, stringsMatcher=fnmatchMatcher):
self.match = stringsMatcher
self.allowRules = allowRules
self.roleMatchers = roleMatchers

def isUserAllowed(self, ep, action, userDetails):
return True

1 change: 1 addition & 0 deletions master/buildbot/www/authz/base.py
@@ -0,0 +1 @@

29 changes: 29 additions & 0 deletions master/buildbot/www/authz/endpointmatchers.py
@@ -0,0 +1,29 @@

class EndpointMatcherBase(object):
def __init__(self, **kwargs):
pass


class AnyEndpointMatcher(EndpointMatcherBase):
def __init__(self, **kwargs):
EndpointMatcherBase.__init__(self, **kwargs)


class ViewBuildsEndpointMatcher(EndpointMatcherBase):
def __init__(self, **kwargs):
EndpointMatcherBase.__init__(self, **kwargs)


class StopBuildEndpointMatcher(EndpointMatcherBase):
def __init__(self, **kwargs):
EndpointMatcherBase.__init__(self, **kwargs)


class BranchEndpointMatcher(EndpointMatcherBase):
def __init__(self, **kwargs):
EndpointMatcherBase.__init__(self, **kwargs)


class ForceBuildEndpointMatcher(EndpointMatcherBase):
def __init__(self, **kwargs):
EndpointMatcherBase.__init__(self, **kwargs)
47 changes: 47 additions & 0 deletions master/buildbot/www/authz/roles.py
@@ -0,0 +1,47 @@

class RolesFromBase(object):
def __init__(self):
pass

def getRolesFromUser(self, userDetails, owner):
return []


class RolesFromGroups(RolesFromBase):
def __init__(self, groupPrefix=""):
RolesFromBase.__init__(self)
self.groupPrefix = groupPrefix

def getRolesFromUser(self, userDetails, owner):
roles = []
if 'groups' in userDetails:
for group in userDetails['groups']:
if group.startsWith(self.groupPrefix):
roles.append(group[len(self.groupPrefix):])
return roles


class RolesFromEmails(RolesFromBase):
def __init__(self, **kwargs):
RolesFromBase.__init__(self)
self.roles = {}
for role, emails in kwargs.iteritems():
for email in emails:
self.roles.setdefault(email, []).append(role)

def getRolesFromUser(self, userDetails, owner):
if 'email' in userDetails:
return self.roles.get(userDetails['email'], [])
return []


class RolesFromOwner(RolesFromBase):
def __init__(self, role):
RolesFromBase.__init__(self)
self.role = role

def getRolesFromUser(self, userDetails, owner):
if 'email' in userDetails:
if userDetails['email'] == owner and owner is not None:
return self.role
return []
25 changes: 25 additions & 0 deletions master/docs/developer/authz.rst
@@ -0,0 +1,25 @@
Authorization
=============

Buildbot authorization is designed to address the following requirements

- Most of the configuration is only data: We avoid to require user to write callbacks for most of the use cases. This to allow to load the config from yaml or json and eventually do a UI for authorization config.
- Separation of concerns:

* Mapping users to roles
* Mapping roles to REST endpoints.

- Configuration should not need hardcoding endpoint paths.
- Easy to extend

Use cases
---------

- Members of admin group should have access to all resources and actions
- developers can run the "try" builders
- Integrators can run the "merge" builders
- Release team can run the "release" builders
- There are separate teams for different branches or projects, but the roles are identic
- Owners of builds can stop builds or buildrequests
- Secret branch's builds are hidden from people except explicitly authorized

45 changes: 45 additions & 0 deletions master/docs/manual/authz_example.py
@@ -0,0 +1,45 @@
from buildbot.www.authz import Authz
from buildbot.www.authz.roles import RolesFromGroups, RolesFromEmails, RolesFromOwner
from buildbot.www.authz.endpointmatchers import AnyEndpointMatcher
from buildbot.www.authz.endpointmatchers import ForceBuildEndpointMatcher
from buildbot.www.authz.endpointmatchers import BranchEndpointMatcher
from buildbot.www.authz.endpointmatchers import ViewBuildsEndpointMatcher
from buildbot.www.authz.endpointmatchers import StopBuildEndpointMatcher

authz = Authz(
stringsMatcher=Authz.fnmatchMatcher, # simple matcher with '*' glob character
# stringsMatcher = Authz.reMatcher, # if you prefer regular expressions
allowRules=[
# admins can do anything,
# defaultDeny=False: if user does not have the admin role, we continue parsing rules
AnyEndpointMatcher(role="admins", defaultDeny=False),

# rules for viewing builds, builders, step logs
# depending on the sourcestamp or buildername
ViewBuildsEndpointMatcher(branch="secretbranch", role="agents"),
ViewBuildsEndpointMatcher(project="secretproject", role="agents"),
ViewBuildsEndpointMatcher(branch="*", role="*"),
ViewBuildsEndpointMatcher(project="*", role="*"),

StopBuildEndpointMatcher(role="owner"),

# nine-* groups can do stuff on the nine branch
BranchEndpointMatcher(branch="nine", role="nine-*"),
# eight-* groups can do stuff on the eight branch
BranchEndpointMatcher(branch="eight", role="eight-*"),

# *-try groups can start "try" builds
ForceBuildEndpointMatcher(builder="try", role="*-try"),
# *-mergers groups can start "merge" builds
ForceBuildEndpointMatcher(builder="merge", role="*-mergers"),
# *-releasers groups can start "release" builds
ForceBuildEndpointMatcher(builder="release", role="*-releasers"),
],
roleMatchers=[
RolesFromGroups(groupPrefix="buildbot-"),
RolesFromEmails(admins=["homer@springfieldplant.com"],
agents=["007@mi6.uk"]),
RolesFromOwner(role="owner")
]
)
c['www'] = dict(authz=authz)
29 changes: 29 additions & 0 deletions master/docs/manual/cfg-www.rst
Expand Up @@ -372,3 +372,32 @@ Here is an nginx configuration that is known to work (nginx 1.6.2):
proxy_read_timeout 6000s;
}
}
.. _Web-Authorization:

Authorization rules
~~~~~~~~~~~~~~~~~~~

Endpoint matchers
+++++++++++++++++
Endpoint matchers are responsible for creating rules to match REST endpoints, and requiring roles for them.
The following sequence is implemented by each EndpointMatcher class

- Check whether the requested endpoint is supported by this matcher
- Get necessary info from data api, and decides whether it matches.
- Looks if the users has the required role.

Several endpoints matchers are currently implemented

.. py:class:: buildbot.www.authz.endpointmatchers.AnyEndpointMatcher(role)
:param role: The role which grants access to any endpoint.

AnyEndpointMatcher grants all rights to a people with given role (usually "admins")

.. py:class:: buildbot.www.authz.endpointmatchers.ForceBuildEndpointMatcher(role)
:param builder: name of the builder.
:param role: The role needed to get access to such endpoints.

ForceBuildEndpointMatcher grants all rights to a people with given role (usually "admins")

0 comments on commit cde0c9a

Please sign in to comment.