OAuth2 Login for BuildBot

The fantastic buildbot continuous integration framework offers plenty of functionality, among others the option to authenticate with several options. The framework, at the time of this writing, comes with plugins packaged, that locally support NoAuth, UserPasswordAuth, HTPasswdAuth. More interestingly, buildbot also supports GoogleAuth, GitHubAuth GitLabAuth, BitbucketAuth for authentication through an OAuth2 login. Further, there are plugins for RemoteUserAuth or CustomAuth.

Extending BuildBot

However, there is no generic plugin for OAuth2 authentication. The obvious reason for this is, that OAuth2 is an authorisation protocol, but that’s the story of another post. While at first glance the documented classes for our purpose seem to be RemoteUserAuth or CustomAuth, open source allows us to understand how for example the GoogleAuth-class works and give us an idea of what we can do here. So does buildbot come with a base-class OAuth2Auth, that all others inherit from. We can do the same for our goal:

from buildbot.www.oauth2 import OAuth2Auth  # for custom authentication

class PramariAuth(OAuth2Auth):
    name = "Pramari"
    faIcon = "fa-python"

    resourceEndpoint = "https://www.pramari.de"
    authUri = 'https://pramari.de/o/authorize/'
    tokenUri = 'https://pramari.de/o/token/'
    authUriAdditionalParams = dict(scope=" ".join(['userinfo', ]))
    
    def getUserInfoFromOAuthClient(self, c):
        data = self.get(c, '/api/userinfo/')
        return dict(full_name="{}, {}".format(data['last_name'], data['first_name']),
                    username=data['username'],
                    email=data["email"],
        )

    def createSessionFromToken(self, token):
        s = requests.Session()
        s.headers = {'Authorization': 'bearer ' + token['access_token']}
        s.verify = self.sslVerify
        return s

The class requires a few attributes to be set. The obvious are name, that indicates the name of the Authentication Plugin, and faIcon, that allows to chose a FontAwesome-Icon as a representation in the buildbot menubar later. Further, OAuth2 requires an authorisation-endpoint URL and a token-exchange endpoint URL. These are specified by the authUri– and tokenUri-attributes in the above example. The authUriAdditionalParams will specify the OAuth2-scopes that will be requested in the authentication/authorisation-process. Since buildbot will require, at a minimum, some information about the authenticated user, this will contain a space-separated list of scopes. Here it is the “userinfo”-scope.

The class can exist in master.cfg.

Providing UserInfo

On the server end, in this case pramari.de, this example presumes using Jazzbands fantastic oauth2-toolkit and the equally fantastic django-restframework. To provide the userinfo that buildbot will expect, in this case under “/api/userinfo“, djangorestframework can serialize information from the Django User-Model.

class UserSerializer(serializers.ModelSerializer):
    # pylint: disable=R0903
    class Meta:
        model = User
        fields = (
            "pk",
            "username",
            "first_name",
            "last_name",
            "email",
        )

To provide these details, djangorestframework needs to have a view to give out these details:

class UserDetails(views.APIView):
    # authentication_classes = [OAuth2Authentication]
    permission_classes = [TokenHasScope]
    required_scopes = ['userinfo']
    serializer_class = UserSerializer


    def get(self, request, *args, **kwargs):
        userinfo = User.objects.get(pk=self.request.user.pk)
        return response.Response(UserSerializer(userinfo).data)

Finally, add the userinfo-api view to your urls.py configuration, to provide the API Endpoint under /api/userinfo/, as configured in the buildbot-authentication class:

urlpatterns += [
    path(r"api/userinfo/", UserDetails.as_view()),
]

Userinfo is now available to API-clients that provide a token with the ‘userinfo’-scope.

Server Side

Of course, the client needs to authenticate against an authentication server, in our example using Jazzbands fantastic OAuth2-toolkit). We’ll not discuss installation and configuration of the server-side here, the package has fantastic documentation. Provided the toolkit is reachable just as the django-oauth2-toolkit tutorial described it, add a client at http://<hostname>/o/applications/:

Configure OAuth2-Client in Jazzband's OAuth2 toolkit.
OAuth2-Toolkit Configuration

The dialog will auto generate client-id and client-secret, that you will need for a later step. The buildbot-application will be a public application, that works using the authorization-code flow. Redirect-URLs for buildbot are <buildbot-url>/auth/login/. Please replace with your URL in the above example.

OAuth2 Login

Once you have client-id and client-secret, you can go and configure the web interface in master.cfg for buildbot:

['www'] = dict(
        port=8010,
        plugins=dict(waterfall_view={}, console_view={}, grid_view={}, badges={"left_pad": 0, "right_pad": 0, "bord
er_radius": 3, "style": "badgeio"}),
        auth=PramariAuth(
                clientId="********", 
                clientSecret="********"
        ),
        change_hook_dialects={'github': {'secret': 'BRbNFGkfC$os9W)3mXCYer>vf2,j;w2MYfzNRRdMz{HGVa6zbaoPi6fb6Fs?EEp
w'}},
        authz=authz
)

As a last hint in this small exercise, you may want to look into authz, to ensure proper access-limits for remote-authenticated users. Again, buildbot has excellent documentation on this topic, and further examples would be beyond the scope of this article. Here only for the completeness of the example:

authz = util.Authz(
  allowRules=[
    util.AnyControlEndpointMatcher(role="admins", defaultDeny=True),
    util.AnyEndpointMatcher(role="admins", defaultDeny=True),
  ],
  roleMatchers=[
    util.RolesFromEmails(admins=["andreas@neumeier.org"])
  ]
)

There you go. After starting, buildbot now comes with your own one-click login:

oauth2-login
login to buildbot with oauth2-server