PageRenderTime 47ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

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

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