Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Pierre Tardy <tardyp@gmail.com>
- Loading branch information
Showing
9 changed files
with
295 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from buildbot.www.authz.authz import Authz | ||
__all__ = ["Authz"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 [] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters