Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add class to retrieve user permissions on Oauth Login #39

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,35 @@ Resources:

+ [Gitea OAuth2 Provider documentation](https://docs.gitea.io/en-us/oauth2-provider/)
+ [Buildbot OAuth2 documentation](https://docs.buildbot.net/current/developer/cls-auth.html?highlight=oauth2#buildbot.www.oauth2.OAuth2Auth)

## Authorization

You can handle authorization based on user permissions in Gitea organizations.

`master.cfg`

```py
from buildbot.plugins import util
c['www']['auth'] = util.GiteaAuthWithPermissions(
endpoint="https://your-gitea-host",
client_id='oauth2-client-id',
client_secret='oauth2-client-secret')

c['www']['authz'] = util.Authz(
stringsMatcher=util.fnmatchStrMatcher,
allowRules=[
util.endpointmatchers.AnyControlEndpointMatcher(role="admins"),
util.endpointmatchers.StopBuildEndpointMatcher(role="admins"),
util.endpointmatchers.EnableSchedulerEndpointMatcher(role="admins"),
util.endpointmatchers.ForceBuildEndpointMatcher(role="admins"),
util.endpointmatchers.RebuildBuildEndpointMatcher(role="admins"),
],
roleMatchers=[
util.RolesFromGitea(roles=["admins"], orgs=['org1'], teams=["Owners"], permissions=["owner"])
]
)
```

Resources:

+ [Buildbot Authorization documentation](https://docs.buildbot.net/current/manual/configuration/www.html#authorization-rules)
18 changes: 18 additions & 0 deletions buildbot_gitea/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,21 @@ def __init__(self, endpoint, client_id, client_secret, **kwargs):

def getUserInfoFromOAuthClient(self, c):
return self.get(c, '/api/v1/user')


class GiteaAuthWithPermissions(GiteaAuth):
def getUserInfoFromOAuthClient(self, c):
user_info = super(GiteaAuthWithPermissions, self).getUserInfoFromOAuthClient(c)

teams_info = self.get(c, '/api/v1/user/teams')

user_organizations = user_info.setdefault("organizations", {})
for team in teams_info:
org = team.get("organization")
if org is None:
continue
user_organizations.setdefault(
org["name"], {}
)[team["name"]] = team["permission"]

return user_info
56 changes: 56 additions & 0 deletions buildbot_gitea/authz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from buildbot.www.authz.roles import RolesFromBase

class RolesFromGitea(RolesFromBase):
def __init__(self, roles, orgs=None, teams=None, permissions=None):
self.roles = roles
self.orgs = orgs
self.teams = teams
self.permissions = permissions

if not any(e is not None and len(e) > 0 for e in [
self.orgs,
self.teams,
self.permissions,
]):
from buildbot import config
config.error('RolesFromGitea require one valid of orgs, teams or permissions')


def getRolesFromUserPermissions(self, user_permissions):
if self.permissions is None or user_permissions in self.permissions:
return self.roles

return None

def getRolesFromUserTeam(self, user_teams):
check_teams = self.teams if self.teams is not None else user_teams.keys()
for team in check_teams:
user_permissions = user_teams.get(team)
if user_permissions is None:
continue

roles = self.getRolesFromUserPermissions(user_permissions)
if roles is not None:
return roles

return None

def getRolesFromUserOrg(self, user_orgs):
check_orgs = self.orgs if self.orgs is not None else user_orgs.keys()
for org in check_orgs:
user_teams = user_orgs.get(org)
if user_teams is None:
continue

roles = self.getRolesFromUserTeam(user_teams)
if roles is not None:
return roles

return None

def getRolesFromUser(self, userDetails):
user_orgs = userDetails.get("organizations", {})
roles = self.getRolesFromUserOrg(user_orgs)
if roles is not None:
return roles
return []
152 changes: 151 additions & 1 deletion buildbot_gitea/test/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from buildbot.secrets.manager import SecretManager
from buildbot.test.fake.secrets import FakeSecretStorage

from buildbot_gitea.auth import GiteaAuth
from buildbot_gitea.auth import GiteaAuth, GiteaAuthWithPermissions

try:
import requests
Expand Down Expand Up @@ -86,3 +86,153 @@ def test_getGiteaLoginURL_with_secret(self):
exp = ("https://gitea.test/login/oauth/authorize?client_id=secretClientId&"
"redirect_uri=h%3A%2Fa%2Fb%2Fauth%2Flogin&response_type=code")
self.assertEqual(res, exp)


class TestGiteaAuthWithPermissions(TestReactorMixin, www.WwwTestMixin, ConfigErrorsMixin,
unittest.TestCase):

USER = {
'avatar_url': 'http://pic',
'email': 'bar@foo',
'full_name': 'foo bar',
'username': 'bar',
}

ORG1 = {
"id": 1,
"name": "Owners",
"description": "",
"organization": {
"id": 1,
"name": "org1",
"full_name": "Organization 1",
"email": "",
"avatar_url": "https://gitea.com/avatars/sha1",
"description": "",
"website": "",
"location": "",
"visibility": "limited",
"repo_admin_change_team_access": True,
"username": "org1"
},
"includes_all_repositories": True,
"permission": "owner",
"units": [
"repo.code",
"repo.issues",
"repo.ext_issues",
"repo.pulls",
"repo.releases",
"repo.wiki",
"repo.packages",
"repo.ext_wiki",
"repo.projects",
"repo.actions"
],
"units_map": {
"repo.actions": "owner",
"repo.code": "owner",
"repo.ext_issues": "owner",
"repo.ext_wiki": "owner",
"repo.issues": "owner",
"repo.packages": "owner",
"repo.projects": "owner",
"repo.pulls": "owner",
"repo.releases": "owner",
"repo.wiki": "owner"
},
"can_create_org_repo": True
}
ORG2 = {
"id": 2,
"name": "Users",
"description": "Basic Users",
"organization": {
"id": 2,
"name": "org2",
"full_name": "Organization 2",
"email": "",
"avatar_url": "https://gitea.com/avatars/sha1",
"description": "",
"website": "https://confluence.dont-nod.com/display/DEV/",
"location": "",
"visibility": "limited",
"repo_admin_change_team_access": False,
"username": "org2"
},
"includes_all_repositories": False,
"permission": "write",
"units": [
"repo.ext_issues",
"repo.code",
"repo.issues",
"repo.pulls",
"repo.releases",
"repo.wiki",
"repo.ext_wiki"
],
"units_map": {
"repo.code": "write",
"repo.ext_issues": "read",
"repo.ext_wiki": "read",
"repo.issues": "write",
"repo.pulls": "write",
"repo.releases": "write",
"repo.wiki": "write"
},
"can_create_org_repo": False
}

def setUp(self):
self.setup_test_reactor()
if requests is None:
raise unittest.SkipTest("Need to install requests to test oauth2")

self.patch(requests, 'request', mock.Mock(spec=requests.request))
self.patch(requests, 'post', mock.Mock(spec=requests.post))
self.patch(requests, 'get', mock.Mock(spec=requests.get))

self.giteaAuth = GiteaAuthWithPermissions(
'https://gitea.test',
'client-id',
'client-secret')
self._master = master = self.make_master(
url='h:/a/b/', auth=self.giteaAuth)
self.giteaAuth.reconfigAuth(master, master.config)

def mock_gitea_auth_get(session, path):
return {
"/api/v1/user": TestGiteaAuthWithPermissions.USER,
"/api/v1/user/teams": [
TestGiteaAuthWithPermissions.ORG1,
TestGiteaAuthWithPermissions.ORG2,
],
}.get(path)

self.patch(self.giteaAuth, 'get', mock.Mock(
spec=GiteaAuthWithPermissions.get,
side_effect=mock_gitea_auth_get,
))

@defer.inlineCallbacks
def test_getGiteaUserOrgTeamPermissions(self):
# won't be used
fake_session = None
res = yield self.giteaAuth.getUserInfoFromOAuthClient(fake_session)
self.assertDictEqual(
res,
{
'avatar_url': 'http://pic',
'email': 'bar@foo',
'full_name': 'foo bar',
'username': 'bar',
'organizations': {
'org1': {
'Owners': 'owner',
},
'org2': {
'Users': 'write',
},
}
},
)
71 changes: 71 additions & 0 deletions buildbot_gitea/test/test_authz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from twisted.trial import unittest
from buildbot_gitea.authz import RolesFromGitea


class TestGiteaAuthz_RolesFromGitea(unittest.TestCase):

def test_RolesFromGitea_NoMatch(self):
user_detail = {}

role_provider = RolesFromGitea(
roles=["test"],
orgs=["org1"],
)
res = role_provider.getRolesFromUser(user_detail)
self.assertNot(res)

role_provider = RolesFromGitea(
roles=["test"],
teams=["team1"],
)
res = role_provider.getRolesFromUser(user_detail)
self.assertNot(res)

role_provider = RolesFromGitea(
roles=["test"],
permissions=["owner"],
)
res = role_provider.getRolesFromUser(user_detail)
self.assertNot(res)

def test_RolesFromGitea_Match(self):
user_detail = {
'organizations': {
'org1': {
'Owners': 'owner',
},
'org2': {
'Users': 'write',
},
}
}

role_provider = RolesFromGitea(
roles=["test"],
orgs=["org1"],
)
res = role_provider.getRolesFromUser(user_detail)
self.assertEqual(res, ["test"])

role_provider = RolesFromGitea(
roles=["test"],
teams=["Owners"],
)
res = role_provider.getRolesFromUser(user_detail)
self.assertEqual(res, ["test"])

role_provider = RolesFromGitea(
roles=["test"],
permissions=["owner"],
)
res = role_provider.getRolesFromUser(user_detail)
self.assertEqual(res, ["test"])

role_provider = RolesFromGitea(
roles=["test"],
orgs=["org1"],
teams=["Owners"],
permissions=["owner"],
)
res = role_provider.getRolesFromUser(user_detail)
self.assertEqual(res, ["test"])
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
"GiteaStatusPush = buildbot_gitea.reporter:GiteaStatusPush"
],
"buildbot.util": [
"GiteaAuth = buildbot_gitea.auth:GiteaAuth"
"GiteaAuth = buildbot_gitea.auth:GiteaAuth",
"GiteaAuthWithPermissions = buildbot_gitea.auth:GiteaAuthWithPermissions",
"RolesFromGitea = buildbot_gitea.authz:RolesFromGitea"
]
},
classifiers=[
Expand Down