"Fossies" - the Fresh Open Source Software Archive

Member "buildbot-2.3.1/buildbot/www/oauth2.py" (23 May 2019, 14974 Bytes) of package /linux/misc/buildbot-2.3.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "oauth2.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 2.3.0_vs_2.3.1.

    1 # This file is part of Buildbot.  Buildbot is free software: you can
    2 # redistribute it and/or modify it under the terms of the GNU General Public
    3 # License as published by the Free Software Foundation, version 2.
    4 #
    5 # This program is distributed in the hope that it will be useful, but WITHOUT
    6 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    7 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    8 # details.
    9 #
   10 # You should have received a copy of the GNU General Public License along with
   11 # this program; if not, write to the Free Software Foundation, Inc., 51
   12 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
   13 #
   14 # Copyright Buildbot Team Members
   15 
   16 import json
   17 import re
   18 import textwrap
   19 from posixpath import join
   20 from urllib.parse import parse_qs
   21 from urllib.parse import urlencode
   22 
   23 import jinja2
   24 import requests
   25 
   26 from twisted.internet import defer
   27 from twisted.internet import threads
   28 
   29 from buildbot import config
   30 from buildbot.process.properties import Properties
   31 from buildbot.util import bytes2unicode
   32 from buildbot.util.logger import Logger
   33 from buildbot.www import auth
   34 from buildbot.www import resource
   35 
   36 log = Logger()
   37 
   38 
   39 class OAuth2LoginResource(auth.LoginResource):
   40     # disable reconfigResource calls
   41     needsReconfig = False
   42 
   43     def __init__(self, master, _auth):
   44         super().__init__(master)
   45         self.auth = _auth
   46 
   47     def render_POST(self, request):
   48         return self.asyncRenderHelper(request, self.renderLogin)
   49 
   50     @defer.inlineCallbacks
   51     def renderLogin(self, request):
   52         code = request.args.get(b"code", [b""])[0]
   53         if not code:
   54             url = request.args.get(b"redirect", [None])[0]
   55             url = yield self.auth.getLoginURL(url)
   56             raise resource.Redirect(url)
   57 
   58         details = yield self.auth.verifyCode(code)
   59 
   60         if self.auth.userInfoProvider is not None:
   61             infos = yield self.auth.userInfoProvider.getUserInfo(details['username'])
   62             details.update(infos)
   63         session = request.getSession()
   64         session.user_info = details
   65         session.updateSession(request)
   66         state = request.args.get(b"state", [b""])[0]
   67         if state:
   68             for redirect in parse_qs(state).get('redirect', []):
   69                 raise resource.Redirect(self.auth.homeUri + "#" + redirect)
   70         raise resource.Redirect(self.auth.homeUri)
   71 
   72 
   73 class OAuth2Auth(auth.AuthBase):
   74     name = 'oauth2'
   75     getTokenUseAuthHeaders = False
   76     authUri = None
   77     tokenUri = None
   78     grantType = 'authorization_code'
   79     authUriAdditionalParams = {}
   80     tokenUriAdditionalParams = {}
   81     loginUri = None
   82     homeUri = None
   83     sslVerify = None
   84 
   85     def __init__(self,
   86                  clientId, clientSecret, autologin=False, **kwargs):
   87         super().__init__(**kwargs)
   88         self.clientId = clientId
   89         self.clientSecret = clientSecret
   90         self.autologin = autologin
   91 
   92     def reconfigAuth(self, master, new_config):
   93         self.master = master
   94         self.loginUri = join(new_config.buildbotURL, "auth/login")
   95         self.homeUri = new_config.buildbotURL
   96 
   97     def getConfigDict(self):
   98         return dict(name=self.name,
   99                     oauth2=True,
  100                     fa_icon=self.faIcon,
  101                     autologin=self.autologin
  102                     )
  103 
  104     def getLoginResource(self):
  105         return OAuth2LoginResource(self.master, self)
  106 
  107     @defer.inlineCallbacks
  108     def getLoginURL(self, redirect_url):
  109         """
  110         Returns the url to redirect the user to for user consent
  111         """
  112         p = Properties()
  113         p.master = self.master
  114         clientId = yield p.render(self.clientId)
  115         oauth_params = {'redirect_uri': self.loginUri,
  116                         'client_id': clientId, 'response_type': 'code'}
  117         if redirect_url is not None:
  118             oauth_params['state'] = urlencode(dict(redirect=redirect_url))
  119         oauth_params.update(self.authUriAdditionalParams)
  120         sorted_oauth_params = sorted(oauth_params.items(), key=lambda val: val[0])
  121         return "%s?%s" % (self.authUri, urlencode(sorted_oauth_params))
  122 
  123     def createSessionFromToken(self, token):
  124         s = requests.Session()
  125         s.params = {'access_token': token['access_token']}
  126         s.verify = self.sslVerify
  127         return s
  128 
  129     def get(self, session, path):
  130         ret = session.get(self.resourceEndpoint + path)
  131         return ret.json()
  132 
  133     # based on https://github.com/maraujop/requests-oauth
  134     # from Miguel Araujo, augmented to support header based clientSecret
  135     # passing
  136     @defer.inlineCallbacks
  137     def verifyCode(self, code):
  138         # everything in deferToThread is not counted with trial  --coverage :-(
  139         def thd(client_id, client_secret):
  140             url = self.tokenUri
  141             data = {'redirect_uri': self.loginUri, 'code': code,
  142                     'grant_type': self.grantType}
  143             auth = None
  144             if self.getTokenUseAuthHeaders:
  145                 auth = (client_id, client_secret)
  146             else:
  147                 data.update(
  148                     {'client_id': client_id, 'client_secret': client_secret})
  149             data.update(self.tokenUriAdditionalParams)
  150             response = requests.post(
  151                 url, data=data, auth=auth, verify=self.sslVerify)
  152             response.raise_for_status()
  153             responseContent = bytes2unicode(response.content)
  154             try:
  155                 content = json.loads(responseContent)
  156             except ValueError:
  157                 content = parse_qs(responseContent)
  158                 for k, v in content.items():
  159                     content[k] = v[0]
  160             except TypeError:
  161                 content = responseContent
  162 
  163             session = self.createSessionFromToken(content)
  164             return self.getUserInfoFromOAuthClient(session)
  165         p = Properties()
  166         p.master = self.master
  167         client_id = yield p.render(self.clientId)
  168         client_secret = yield p.render(self.clientSecret)
  169         result = yield threads.deferToThread(thd, client_id, client_secret)
  170         return result
  171 
  172     def getUserInfoFromOAuthClient(self, c):
  173         return {}
  174 
  175 
  176 class GoogleAuth(OAuth2Auth):
  177     name = "Google"
  178     faIcon = "fa-google-plus"
  179     resourceEndpoint = "https://www.googleapis.com/oauth2/v1"
  180     authUri = 'https://accounts.google.com/o/oauth2/auth'
  181     tokenUri = 'https://accounts.google.com/o/oauth2/token'
  182     authUriAdditionalParams = dict(scope=" ".join([
  183                                    'https://www.googleapis.com/auth/userinfo.email',
  184                                    'https://www.googleapis.com/auth/userinfo.profile'
  185                                    ]))
  186 
  187     def getUserInfoFromOAuthClient(self, c):
  188         data = self.get(c, '/userinfo')
  189         return dict(full_name=data["name"],
  190                     username=data['email'].split("@")[0],
  191                     email=data["email"],
  192                     avatar_url=data["picture"])
  193 
  194 
  195 class GitHubAuth(OAuth2Auth):
  196     name = "GitHub"
  197     faIcon = "fa-github"
  198     authUri = 'https://github.com/login/oauth/authorize'
  199     authUriAdditionalParams = {'scope': 'user:email read:org'}
  200     tokenUri = 'https://github.com/login/oauth/access_token'
  201     resourceEndpoint = 'https://api.github.com'
  202 
  203     getUserTeamsGraphqlTpl = textwrap.dedent(r'''
  204         {%- if organizations %}
  205         query getOrgTeamMembership {
  206           {%- for org_slug, org_name in organizations.items() %}
  207           {{ org_slug }}: organization(login: "{{ org_name }}") {
  208             teams(first: 100 userLogins: ["{{ user_info.username }}"]) {
  209               edges {
  210                 node {
  211                   name,
  212                   slug
  213                 }
  214               }
  215             }
  216           }
  217           {%- endfor %}
  218         }
  219         {%- endif %}
  220     ''')
  221 
  222     def __init__(self,
  223                  clientId, clientSecret, serverURL=None, autologin=False,
  224                  apiVersion=3, getTeamsMembership=False, debug=False,
  225                  **kwargs):
  226 
  227         super().__init__(clientId, clientSecret, autologin, **kwargs)
  228         if serverURL is not None:
  229             # setup for enterprise github
  230             if serverURL.endswith("/"):
  231                 serverURL = serverURL[:-1]
  232             # v3 is accessible directly at /api/v3 for enterprise, but directly for SaaS..
  233             self.resourceEndpoint = serverURL + '/api/v3'
  234 
  235             self.authUri = '{0}/login/oauth/authorize'.format(serverURL)
  236             self.tokenUri = '{0}/login/oauth/access_token'.format(serverURL)
  237         self.serverURL = serverURL or self.resourceEndpoint
  238 
  239         if apiVersion not in (3, 4):
  240             config.error(
  241                 'GitHubAuth apiVersion must be 3 or 4 not {}'.format(
  242                     apiVersion))
  243         self.apiVersion = apiVersion
  244         if apiVersion == 3:
  245             if getTeamsMembership is True:
  246                 config.error(
  247                     'Retrieving team membership information using GitHubAuth is only '
  248                     'possible using GitHub api v4.')
  249         else:
  250             self.apiResourceEndpoint = self.serverURL + '/graphql'
  251         if getTeamsMembership:
  252             # GraphQL name aliases must comply with /^[_a-zA-Z][_a-zA-Z0-9]*$/
  253             self._orgname_slug_sub_re = re.compile(r'[^_a-zA-Z0-9]')
  254             self.getUserTeamsGraphqlTplC = jinja2.Template(
  255                 self.getUserTeamsGraphqlTpl.strip())
  256         self.getTeamsMembership = getTeamsMembership
  257         self.debug = debug
  258 
  259     def post(self, session, query):
  260         if self.debug:
  261             log.info('{klass} GraphQL POST Request: {endpoint} -> '
  262                      'DATA:\n----\n{data}\n----',
  263                      klass=self.__class__.__name__,
  264                      endpoint=self.apiResourceEndpoint,
  265                      data=query)
  266         ret = session.post(self.apiResourceEndpoint, json={'query': query})
  267         return ret.json()
  268 
  269     def getUserInfoFromOAuthClient(self, c):
  270         if self.apiVersion == 3:
  271             return self.getUserInfoFromOAuthClient_v3(c)
  272         return self.getUserInfoFromOAuthClient_v4(c)
  273 
  274     def getUserInfoFromOAuthClient_v3(self, c):
  275         user = self.get(c, '/user')
  276         emails = self.get(c, '/user/emails')
  277         for email in emails:
  278             if email.get('primary', False):
  279                 user['email'] = email['email']
  280                 break
  281         orgs = self.get(c, '/user/orgs')
  282         return dict(full_name=user['name'],
  283                     email=user['email'],
  284                     username=user['login'],
  285                     groups=[org['login'] for org in orgs])
  286 
  287     def getUserInfoFromOAuthClient_v4(self, c):
  288         graphql_query = textwrap.dedent('''
  289             query {
  290               viewer {
  291                 email
  292                 login
  293                 name
  294                 organizations(first: 100) {
  295                   edges {
  296                     node {
  297                       login
  298                     }
  299                   }
  300                 }
  301               }
  302             }
  303         ''')
  304         data = self.post(c, graphql_query.strip())
  305         data = data['data']
  306         if self.debug:
  307             log.info('{klass} GraphQL Response: {response}',
  308                      klass=self.__class__.__name__,
  309                      response=data)
  310         user_info = dict(full_name=data['viewer']['name'],
  311                          email=data['viewer']['email'],
  312                          username=data['viewer']['login'],
  313                          groups=[org['node']['login'] for org in
  314                                  data['viewer']['organizations']['edges']])
  315         if self.getTeamsMembership:
  316             orgs_name_slug_mapping = {
  317                 self._orgname_slug_sub_re.sub('_', n): n
  318                 for n in user_info['groups']}
  319             graphql_query = self.getUserTeamsGraphqlTplC.render(
  320                 {'user_info': user_info,
  321                  'organizations': orgs_name_slug_mapping})
  322             if graphql_query:
  323                 data = self.post(c, graphql_query)
  324                 if self.debug:
  325                     log.info('{klass} GraphQL Response: {response}',
  326                              klass=self.__class__.__name__,
  327                              response=data)
  328                 teams = set()
  329                 for org, team_data in data['data'].items():
  330                     if team_data is None:
  331                         # Organizations can have OAuth App access restrictions enabled,
  332                         # disallowing team data access to third-parties.
  333                         continue
  334                     for node in team_data['teams']['edges']:
  335                         # On github we can mentions organization teams like
  336                         # @org-name/team-name. Let's keep the team formatting
  337                         # identical with the inclusion of the organization
  338                         # since different organizations might share a common
  339                         # team name
  340                         teams.add('%s/%s' % (orgs_name_slug_mapping[org], node['node']['name']))
  341                         teams.add('%s/%s' % (orgs_name_slug_mapping[org], node['node']['slug']))
  342                 user_info['groups'].extend(sorted(teams))
  343         if self.debug:
  344             log.info('{klass} User Details: {user_info}',
  345                      klass=self.__class__.__name__,
  346                      user_info=user_info)
  347         return user_info
  348 
  349 
  350 class GitLabAuth(OAuth2Auth):
  351     name = "GitLab"
  352     faIcon = "fa-git"
  353 
  354     def __init__(self, instanceUri, clientId, clientSecret, **kwargs):
  355         uri = instanceUri.rstrip("/")
  356         self.authUri = "%s/oauth/authorize" % uri
  357         self.tokenUri = "%s/oauth/token" % uri
  358         self.resourceEndpoint = "%s/api/v4" % uri
  359         super(GitLabAuth, self).__init__(clientId, clientSecret, **kwargs)
  360 
  361     def getUserInfoFromOAuthClient(self, c):
  362         user = self.get(c, "/user")
  363         groups = self.get(c, "/groups")
  364         return dict(full_name=user["name"],
  365                     username=user["username"],
  366                     email=user["email"],
  367                     avatar_url=user["avatar_url"],
  368                     groups=[g["path"] for g in groups])
  369 
  370 
  371 class BitbucketAuth(OAuth2Auth):
  372     name = "Bitbucket"
  373     faIcon = "fa-bitbucket"
  374     authUri = 'https://bitbucket.org/site/oauth2/authorize'
  375     tokenUri = 'https://bitbucket.org/site/oauth2/access_token'
  376     resourceEndpoint = 'https://api.bitbucket.org/2.0'
  377 
  378     def getUserInfoFromOAuthClient(self, c):
  379         user = self.get(c, '/user')
  380         emails = self.get(c, '/user/emails')
  381         for email in emails["values"]:
  382             if email.get('is_primary', False):
  383                 user['email'] = email['email']
  384                 break
  385         orgs = self.get(c, '/teams?role=member')
  386         return dict(full_name=user['display_name'],
  387                     email=user['email'],
  388                     username=user['username'],
  389                     groups=[org['username'] for org in orgs["values"]])