PageRenderTime 54ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/master/buildbot/test/unit/test_www_oauth.py

https://github.com/djmitche/buildbot
Python | 557 lines | 468 code | 51 blank | 38 comment | 11 complexity | a47015e80ce968d317d162978d4f56ca MD5 | raw file
Possible License(s): GPL-2.0
  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. from __future__ import absolute_import
  16. from __future__ import print_function
  17. import json
  18. import os
  19. import webbrowser
  20. import mock
  21. from twisted.internet import defer
  22. from twisted.internet import reactor
  23. from twisted.internet import threads
  24. from twisted.python import failure
  25. from twisted.trial import unittest
  26. from twisted.web.resource import Resource
  27. from twisted.web.server import Site
  28. from buildbot.test.util import www
  29. from buildbot.test.util.config import ConfigErrorsMixin
  30. from buildbot.util import bytes2NativeString
  31. try:
  32. import requests
  33. except ImportError:
  34. requests = None
  35. if requests:
  36. from buildbot.www import oauth2 # pylint: disable=ungrouped-imports
  37. class FakeResponse(object):
  38. def __init__(self, _json):
  39. self.json = lambda: _json
  40. self.content = json.dumps(_json)
  41. def raise_for_status(self):
  42. pass
  43. class OAuth2Auth(www.WwwTestMixin, ConfigErrorsMixin, unittest.TestCase):
  44. def setUp(self):
  45. if requests is None:
  46. raise unittest.SkipTest("Need to install requests to test oauth2")
  47. self.patch(requests, 'request', mock.Mock(spec=requests.request))
  48. self.patch(requests, 'post', mock.Mock(spec=requests.post))
  49. self.patch(requests, 'get', mock.Mock(spec=requests.get))
  50. self.googleAuth = oauth2.GoogleAuth("ggclientID", "clientSECRET")
  51. self.githubAuth = oauth2.GitHubAuth("ghclientID", "clientSECRET")
  52. self.githubAuth_v4 = oauth2.GitHubAuth(
  53. "ghclientID", "clientSECRET", apiVersion=4)
  54. self.githubAuth_v4_teams = oauth2.GitHubAuth(
  55. "ghclientID", "clientSECRET", apiVersion=4, getTeamsMembership=True)
  56. self.githubAuthEnt = oauth2.GitHubAuth(
  57. "ghclientID", "clientSECRET", serverURL="https://git.corp.fakecorp.com")
  58. self.gitlabAuth = oauth2.GitLabAuth(
  59. "https://gitlab.test/", "glclientID", "clientSECRET")
  60. self.bitbucketAuth = oauth2.BitbucketAuth("bbclientID", "clientSECRET")
  61. for auth in [self.googleAuth, self.githubAuth, self.githubAuth_v4, self.githubAuth_v4_teams,
  62. self.githubAuthEnt, self.gitlabAuth, self.bitbucketAuth]:
  63. self._master = master = self.make_master(url='h:/a/b/', auth=auth)
  64. auth.reconfigAuth(master, master.config)
  65. @defer.inlineCallbacks
  66. def test_getGoogleLoginURL(self):
  67. res = yield self.googleAuth.getLoginURL('http://redir')
  68. exp = ("https://accounts.google.com/o/oauth2/auth?client_id=ggclientID&"
  69. "redirect_uri=h%3A%2Fa%2Fb%2Fauth%2Flogin&response_type=code&"
  70. "scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+"
  71. "https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&"
  72. "state=redirect%3Dhttp%253A%252F%252Fredir")
  73. self.assertEqual(res, exp)
  74. res = yield self.googleAuth.getLoginURL(None)
  75. exp = ("https://accounts.google.com/o/oauth2/auth?client_id=ggclientID&"
  76. "redirect_uri=h%3A%2Fa%2Fb%2Fauth%2Flogin&response_type=code&"
  77. "scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+"
  78. "https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile")
  79. self.assertEqual(res, exp)
  80. @defer.inlineCallbacks
  81. def test_getGithubLoginURL(self):
  82. res = yield self.githubAuth.getLoginURL('http://redir')
  83. exp = ("https://github.com/login/oauth/authorize?client_id=ghclientID&"
  84. "redirect_uri=h%3A%2Fa%2Fb%2Fauth%2Flogin&response_type=code&"
  85. "scope=user%3Aemail+read%3Aorg&"
  86. "state=redirect%3Dhttp%253A%252F%252Fredir")
  87. self.assertEqual(res, exp)
  88. res = yield self.githubAuth.getLoginURL(None)
  89. exp = ("https://github.com/login/oauth/authorize?client_id=ghclientID&"
  90. "redirect_uri=h%3A%2Fa%2Fb%2Fauth%2Flogin&response_type=code&"
  91. "scope=user%3Aemail+read%3Aorg")
  92. self.assertEqual(res, exp)
  93. @defer.inlineCallbacks
  94. def test_getGithubELoginURL(self):
  95. res = yield self.githubAuthEnt.getLoginURL('http://redir')
  96. exp = ("https://git.corp.fakecorp.com/login/oauth/authorize?client_id=ghclientID&"
  97. "redirect_uri=h%3A%2Fa%2Fb%2Fauth%2Flogin&response_type=code&"
  98. "scope=user%3Aemail+read%3Aorg&"
  99. "state=redirect%3Dhttp%253A%252F%252Fredir")
  100. self.assertEqual(res, exp)
  101. res = yield self.githubAuthEnt.getLoginURL(None)
  102. exp = ("https://git.corp.fakecorp.com/login/oauth/authorize?client_id=ghclientID&"
  103. "redirect_uri=h%3A%2Fa%2Fb%2Fauth%2Flogin&response_type=code&"
  104. "scope=user%3Aemail+read%3Aorg")
  105. self.assertEqual(res, exp)
  106. @defer.inlineCallbacks
  107. def test_getGitLabLoginURL(self):
  108. res = yield self.gitlabAuth.getLoginURL('http://redir')
  109. exp = ("https://gitlab.test/oauth/authorize"
  110. "?client_id=glclientID&"
  111. "redirect_uri=h%3A%2Fa%2Fb%2Fauth%2Flogin&"
  112. "response_type=code&"
  113. "state=redirect%3Dhttp%253A%252F%252Fredir")
  114. self.assertEqual(res, exp)
  115. res = yield self.gitlabAuth.getLoginURL(None)
  116. exp = ("https://gitlab.test/oauth/authorize"
  117. "?client_id=glclientID&"
  118. "redirect_uri=h%3A%2Fa%2Fb%2Fauth%2Flogin&"
  119. "response_type=code")
  120. self.assertEqual(res, exp)
  121. @defer.inlineCallbacks
  122. def test_getBitbucketLoginURL(self):
  123. res = yield self.bitbucketAuth.getLoginURL('http://redir')
  124. exp = ("https://bitbucket.org/site/oauth2/authorize?"
  125. "client_id=bbclientID&"
  126. "redirect_uri=h%3A%2Fa%2Fb%2Fauth%2Flogin&"
  127. "response_type=code&"
  128. "state=redirect%3Dhttp%253A%252F%252Fredir")
  129. self.assertEqual(res, exp)
  130. res = yield self.bitbucketAuth.getLoginURL(None)
  131. exp = ("https://bitbucket.org/site/oauth2/authorize?"
  132. "client_id=bbclientID&"
  133. "redirect_uri=h%3A%2Fa%2Fb%2Fauth%2Flogin&"
  134. "response_type=code")
  135. self.assertEqual(res, exp)
  136. @defer.inlineCallbacks
  137. def test_GoogleVerifyCode(self):
  138. requests.get.side_effect = []
  139. requests.post.side_effect = [
  140. FakeResponse(dict(access_token="TOK3N"))]
  141. self.googleAuth.get = mock.Mock(side_effect=[dict(
  142. name="foo bar",
  143. email="bar@foo", picture="http://pic")])
  144. res = yield self.googleAuth.verifyCode("code!")
  145. self.assertEqual({'avatar_url': 'http://pic', 'email': 'bar@foo',
  146. 'full_name': 'foo bar', 'username': 'bar'}, res)
  147. @defer.inlineCallbacks
  148. def test_GithubVerifyCode(self):
  149. requests.get.side_effect = []
  150. requests.post.side_effect = [
  151. FakeResponse(dict(access_token="TOK3N"))]
  152. self.githubAuth.get = mock.Mock(side_effect=[
  153. dict( # /user
  154. login="bar",
  155. name="foo bar",
  156. email="buzz@bar"),
  157. [ # /user/emails
  158. {'email': 'buzz@bar', 'verified': True, 'primary': False},
  159. {'email': 'bar@foo', 'verified': True, 'primary': True}],
  160. [ # /user/orgs
  161. dict(login="hello"),
  162. dict(login="grp"),
  163. ]])
  164. res = yield self.githubAuth.verifyCode("code!")
  165. self.assertEqual({'email': 'bar@foo',
  166. 'username': 'bar',
  167. 'groups': ["hello", "grp"],
  168. 'full_name': 'foo bar'}, res)
  169. @defer.inlineCallbacks
  170. def test_GithubAcceptToken(self):
  171. requests.get.side_effect = []
  172. requests.post.side_effect = [
  173. FakeResponse(dict(access_token="TOK3N"))]
  174. self.githubAuth.get = mock.Mock(side_effect=[
  175. dict( # /user
  176. login="bar",
  177. name="foo bar",
  178. email="buzz@bar"),
  179. [ # /user/emails
  180. {'email': 'buzz@bar', 'verified': True, 'primary': False},
  181. {'email': 'bar@foo', 'verified': True, 'primary': True}],
  182. [ # /user/orgs
  183. dict(login="hello"),
  184. dict(login="grp"),
  185. ]])
  186. res = yield self.githubAuth.acceptToken("TOK3N")
  187. self.assertEqual({'email': 'bar@foo',
  188. 'username': 'bar',
  189. 'groups': ["hello", "grp"],
  190. 'full_name': 'foo bar'}, res)
  191. @defer.inlineCallbacks
  192. def test_GithubAcceptToken_v4(self):
  193. requests.get.side_effect = []
  194. requests.post.side_effect = [
  195. FakeResponse(dict(access_token="TOK3N"))]
  196. self.githubAuth_v4.post = mock.Mock(side_effect=[
  197. {
  198. 'data': {
  199. 'viewer': {
  200. 'organizations': {
  201. 'edges': [
  202. {
  203. 'node': {
  204. 'login': 'hello'
  205. }
  206. },
  207. {
  208. 'node': {
  209. 'login': 'grp'
  210. }
  211. }
  212. ]
  213. },
  214. 'login': 'bar',
  215. 'email': 'bar@foo',
  216. 'name': 'foo bar'
  217. }
  218. }
  219. }
  220. ])
  221. res = yield self.githubAuth_v4.acceptToken("TOK3N")
  222. self.assertEqual({'email': 'bar@foo',
  223. 'username': 'bar',
  224. 'groups': ["hello", "grp"],
  225. 'full_name': 'foo bar'}, res)
  226. @defer.inlineCallbacks
  227. def test_GithubAcceptToken_v4_teams(self):
  228. requests.get.side_effect = []
  229. requests.post.side_effect = [
  230. FakeResponse(dict(access_token="TOK3N"))]
  231. self.githubAuth_v4_teams.post = mock.Mock(side_effect=[
  232. {
  233. 'data': {
  234. 'viewer': {
  235. 'organizations': {
  236. 'edges': [
  237. {
  238. 'node': {
  239. 'login': 'hello'
  240. }
  241. },
  242. {
  243. 'node': {
  244. 'login': 'grp'
  245. }
  246. }
  247. ]
  248. },
  249. 'login': 'bar',
  250. 'email': 'bar@foo',
  251. 'name': 'foo bar'
  252. }
  253. }
  254. },
  255. {
  256. 'data': {
  257. 'hello': {
  258. 'teams': {
  259. 'edges': [
  260. {
  261. 'node': {
  262. 'name': 'developers',
  263. 'slug': 'develpers'
  264. }
  265. },
  266. {
  267. 'node': {
  268. 'name': 'contributors',
  269. 'slug': 'contributors'
  270. }
  271. }
  272. ]
  273. }
  274. },
  275. 'grp': {
  276. 'teams': {
  277. 'edges': [
  278. {
  279. 'node': {
  280. 'name': 'developers',
  281. 'slug': 'develpers'
  282. }
  283. },
  284. {
  285. 'node': {
  286. 'name': 'contributors',
  287. 'slug': 'contributors'
  288. }
  289. },
  290. {
  291. 'node': {
  292. 'name': 'committers',
  293. 'slug': 'committers'
  294. }
  295. },
  296. ]
  297. }
  298. },
  299. }
  300. }
  301. ])
  302. res = yield self.githubAuth_v4_teams.acceptToken("TOK3N")
  303. self.assertEqual({'email': 'bar@foo',
  304. 'username': 'bar',
  305. 'groups': [
  306. 'hello',
  307. 'grp',
  308. 'grp/committers',
  309. 'grp/contributors',
  310. 'grp/developers',
  311. 'hello/contributors',
  312. 'hello/developers'
  313. ],
  314. 'full_name': 'foo bar'}, res)
  315. def test_GitHubAuthBadApiVersion(self):
  316. for bad_api_version in (2, 5, 'a'):
  317. self.assertRaisesConfigError(
  318. 'GitHubAuth apiVersion must be 3 or 4 not ',
  319. lambda api_version=bad_api_version: oauth2.GitHubAuth(
  320. "ghclientID", "clientSECRET", apiVersion=api_version)
  321. )
  322. def test_GitHubAuthRaiseErrorWithApiV3AndGetTeamMembership(self):
  323. self.assertRaisesConfigError(
  324. 'Retrieving team membership information using GitHubAuth is only possible using GitHub api v4.',
  325. lambda: oauth2.GitHubAuth("ghclientID", "clientSECRET", apiVersion=3, getTeamsMembership=True)
  326. )
  327. @defer.inlineCallbacks
  328. def test_GitlabVerifyCode(self):
  329. requests.get.side_effect = []
  330. requests.post.side_effect = [
  331. FakeResponse(dict(access_token="TOK3N"))]
  332. self.gitlabAuth.get = mock.Mock(side_effect=[
  333. { # /user
  334. "name": "Foo Bar",
  335. "username": "fbar",
  336. "id": 5,
  337. "avatar_url": "https://avatar/fbar.png",
  338. "email": "foo@bar",
  339. "twitter": "fb",
  340. },
  341. [ # /groups
  342. {"id": 10, "name": "Hello", "path": "hello"},
  343. {"id": 20, "name": "Group", "path": "grp"},
  344. ]])
  345. res = yield self.gitlabAuth.verifyCode("code!")
  346. self.assertEqual({"full_name": "Foo Bar",
  347. "username": "fbar",
  348. "email": "foo@bar",
  349. "avatar_url": "https://avatar/fbar.png",
  350. "groups": ["hello", "grp"]}, res)
  351. @defer.inlineCallbacks
  352. def test_BitbucketVerifyCode(self):
  353. requests.get.side_effect = []
  354. requests.post.side_effect = [
  355. FakeResponse(dict(access_token="TOK3N"))]
  356. self.bitbucketAuth.get = mock.Mock(side_effect=[
  357. dict( # /user
  358. username="bar",
  359. display_name="foo bar"),
  360. dict( # /user/emails
  361. values=[
  362. {'email': 'buzz@bar', 'is_primary': False},
  363. {'email': 'bar@foo', 'is_primary': True}]),
  364. dict( # /teams?role=member
  365. values=[
  366. {'username': 'hello'},
  367. {'username': 'grp'}])
  368. ])
  369. res = yield self.bitbucketAuth.verifyCode("code!")
  370. self.assertEqual({'email': 'bar@foo',
  371. 'username': 'bar',
  372. "groups": ["hello", "grp"],
  373. 'full_name': 'foo bar'}, res)
  374. @defer.inlineCallbacks
  375. def test_loginResource(self):
  376. class fakeAuth(object):
  377. homeUri = "://me"
  378. getLoginURL = mock.Mock(side_effect=lambda x: defer.succeed("://"))
  379. verifyCode = mock.Mock(
  380. side_effect=lambda code: defer.succeed({"username": "bar"}))
  381. acceptToken = mock.Mock(
  382. side_effect=lambda token: defer.succeed({"username": "bar"}))
  383. userInfoProvider = None
  384. rsrc = self.githubAuth.getLoginResource()
  385. rsrc.auth = fakeAuth()
  386. res = yield self.render_resource(rsrc, b'/')
  387. rsrc.auth.getLoginURL.assert_called_once_with(None)
  388. rsrc.auth.verifyCode.assert_not_called()
  389. self.assertEqual(res, {'redirected': '://'})
  390. rsrc.auth.getLoginURL.reset_mock()
  391. rsrc.auth.verifyCode.reset_mock()
  392. res = yield self.render_resource(rsrc, b'/?code=code!')
  393. rsrc.auth.getLoginURL.assert_not_called()
  394. rsrc.auth.verifyCode.assert_called_once_with(b"code!")
  395. self.assertEqual(self.master.session.user_info, {'username': 'bar'})
  396. self.assertEqual(res, {'redirected': '://me'})
  397. res = yield self.render_resource(rsrc, b'/?token=token!')
  398. rsrc.auth.getLoginURL.assert_not_called()
  399. rsrc.auth.acceptToken.assert_called_once_with(b"token!")
  400. self.assertEqual(self.master.session.user_info, {'username': 'bar'})
  401. self.assertEqual(res, {'redirected': '://me'})
  402. def test_getConfig(self):
  403. self.assertEqual(self.githubAuth.getConfigDict(), {'fa_icon': 'fa-github', 'autologin': False,
  404. 'name': 'GitHub', 'oauth2': True})
  405. self.assertEqual(self.googleAuth.getConfigDict(), {'fa_icon': 'fa-google-plus', 'autologin': False,
  406. 'name': 'Google', 'oauth2': True})
  407. self.assertEqual(self.gitlabAuth.getConfigDict(), {'fa_icon': 'fa-git', 'autologin': False,
  408. 'name': 'GitLab', 'oauth2': True})
  409. self.assertEqual(self.bitbucketAuth.getConfigDict(), {'fa_icon': 'fa-bitbucket', 'autologin': False,
  410. 'name': 'Bitbucket', 'oauth2': True})
  411. # unit tests are not very useful to write new oauth support
  412. # so following is an e2e test, which opens a browser, and do the oauth
  413. # negotiation. The browser window close in the end of the test
  414. # in order to use this tests, you need to create Github/Google ClientID (see doc on how to do it)
  415. # point OAUTHCONF environment variable to a file with following params:
  416. # {
  417. # "GitHubAuth": {
  418. # "CLIENTID": "XX
  419. # "CLIENTSECRET": "XX"
  420. # },
  421. # "GoogleAuth": {
  422. # "CLIENTID": "XX",
  423. # "CLIENTSECRET": "XX"
  424. # }
  425. # "GitLabAuth": {
  426. # "INSTANCEURI": "XX",
  427. # "CLIENTID": "XX",
  428. # "CLIENTSECRET": "XX"
  429. # }
  430. # }
  431. class OAuth2AuthGitHubE2E(www.WwwTestMixin, unittest.TestCase):
  432. authClass = "GitHubAuth"
  433. def _instantiateAuth(self, cls, config):
  434. return cls(config["CLIENTID"], config["CLIENTSECRET"])
  435. def setUp(self):
  436. if requests is None:
  437. raise unittest.SkipTest("Need to install requests to test oauth2")
  438. if "OAUTHCONF" not in os.environ:
  439. raise unittest.SkipTest(
  440. "Need to pass OAUTHCONF path to json file via environ to run this e2e test")
  441. with open(os.environ['OAUTHCONF']) as f:
  442. jsonData = f.read()
  443. config = json.loads(jsonData)[self.authClass]
  444. from buildbot.www import oauth2
  445. self.auth = self._instantiateAuth(
  446. getattr(oauth2, self.authClass), config)
  447. # 5000 has to be hardcoded, has oauth clientids are bound to a fully
  448. # classified web site
  449. master = self.make_master(url='http://localhost:5000/', auth=self.auth)
  450. self.auth.reconfigAuth(master, master.config)
  451. def tearDown(self):
  452. from twisted.internet.tcp import Server
  453. # browsers has the bad habit on not closing the persistent
  454. # connections, so we need to hack them away to make trial happy
  455. f = failure.Failure(Exception("test end"))
  456. for reader in reactor.getReaders():
  457. if isinstance(reader, Server):
  458. reader.connectionLost(f)
  459. @defer.inlineCallbacks
  460. def test_E2E(self):
  461. d = defer.Deferred()
  462. import twisted
  463. twisted.web.http._logDateTimeUsers = 1
  464. class HomePage(Resource):
  465. isLeaf = True
  466. def render_GET(self, request):
  467. info = request.getSession().user_info
  468. reactor.callLater(0, d.callback, info)
  469. return (b"<html><script>setTimeout(close,1000)</script><body>WORKED: " +
  470. info + b"</body></html>")
  471. class MySite(Site):
  472. def makeSession(self):
  473. uid = self._mkuid()
  474. session = self.sessions[uid] = self.sessionFactory(self, uid)
  475. return session
  476. root = Resource()
  477. root.putChild(b"", HomePage())
  478. auth = Resource()
  479. root.putChild(b'auth', auth)
  480. auth.putChild(b'login', self.auth.getLoginResource())
  481. site = MySite(root)
  482. listener = reactor.listenTCP(5000, site)
  483. def thd():
  484. res = requests.get('http://localhost:5000/auth/login')
  485. content = bytes2NativeString(res.content)
  486. webbrowser.open(content)
  487. threads.deferToThread(thd)
  488. res = yield d
  489. yield listener.stopListening()
  490. yield site.stopFactory()
  491. self.assertIn("full_name", res)
  492. self.assertIn("email", res)
  493. self.assertIn("username", res)
  494. class OAuth2AuthGoogleE2E(OAuth2AuthGitHubE2E):
  495. authClass = "GoogleAuth"
  496. class OAuth2AuthGitLabE2E(OAuth2AuthGitHubE2E):
  497. authClass = "GitLabAuth"
  498. def _instantiateAuth(self, cls, config):
  499. return cls(config["INSTANCEURI"], config["CLIENTID"], config["CLIENTSECRET"])