The Gatekeeper, or a project gating system
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

2223 lines
87 KiB

  1. # Copyright 2014 Hewlett-Packard Development Company, L.P.
  2. # Copyright 2014 Rackspace Australia
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. import json
  16. import os
  17. import urllib.parse
  18. import socket
  19. import textwrap
  20. import time
  21. import jwt
  22. import sys
  23. import subprocess
  24. import requests
  25. import zuul.web
  26. import zuul.rpcclient
  27. from tests.base import ZuulTestCase, ZuulDBTestCase, AnsibleZuulTestCase
  28. from tests.base import ZuulWebFixture, FIXTURE_DIR, iterate_timeout
  29. from tests.base import simple_layout
  30. class FakeConfig(object):
  31. def __init__(self, config):
  32. self.config = config or {}
  33. def has_option(self, section, option):
  34. return option in self.config.get(section, {})
  35. def get(self, section, option):
  36. return self.config.get(section, {}).get(option)
  37. class BaseTestWeb(ZuulTestCase):
  38. tenant_config_file = 'config/single-tenant/main.yaml'
  39. config_ini_data = {}
  40. def setUp(self):
  41. super(BaseTestWeb, self).setUp()
  42. self.zuul_ini_config = FakeConfig(self.config_ini_data)
  43. # Start the web server
  44. self.web = self.useFixture(
  45. ZuulWebFixture(
  46. self.gearman_server.port,
  47. self.config,
  48. self.test_root,
  49. info=zuul.model.WebInfo.fromConfig(self.zuul_ini_config),
  50. zk_hosts=self.zk_config))
  51. self.executor_server.hold_jobs_in_build = True
  52. self.host = 'localhost'
  53. self.port = self.web.port
  54. # Wait until web server is started
  55. while True:
  56. try:
  57. with socket.create_connection((self.host, self.port)):
  58. break
  59. except ConnectionRefusedError:
  60. pass
  61. self.base_url = "http://{host}:{port}".format(
  62. host=self.host, port=self.port)
  63. def add_base_changes(self):
  64. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  65. A.addApproval('Code-Review', 2)
  66. self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
  67. B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
  68. B.addApproval('Code-Review', 2)
  69. self.fake_gerrit.addEvent(B.addApproval('Approved', 1))
  70. self.waitUntilSettled()
  71. def get_url(self, url, *args, **kwargs):
  72. return requests.get(
  73. urllib.parse.urljoin(self.base_url, url), *args, **kwargs)
  74. def post_url(self, url, *args, **kwargs):
  75. return requests.post(
  76. urllib.parse.urljoin(self.base_url, url), *args, **kwargs)
  77. def delete_url(self, url, *args, **kwargs):
  78. return requests.delete(
  79. urllib.parse.urljoin(self.base_url, url), *args, **kwargs)
  80. def options_url(self, url, *args, **kwargs):
  81. return requests.options(
  82. urllib.parse.urljoin(self.base_url, url), *args, **kwargs)
  83. def tearDown(self):
  84. self.executor_server.hold_jobs_in_build = False
  85. self.executor_server.release()
  86. self.waitUntilSettled()
  87. super(BaseTestWeb, self).tearDown()
  88. class TestWeb(BaseTestWeb):
  89. def test_web_index(self):
  90. "Test that we can retrieve the index page"
  91. resp = self.get_url('api')
  92. data = resp.json()
  93. # no point checking the whole thing; just make sure _something_ we
  94. # expect is here
  95. self.assertIn('info', data)
  96. def test_web_status(self):
  97. "Test that we can retrieve JSON status info"
  98. self.add_base_changes()
  99. self.executor_server.hold_jobs_in_build = True
  100. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  101. A.addApproval('Code-Review', 2)
  102. self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
  103. self.waitUntilSettled()
  104. self.executor_server.release('project-merge')
  105. self.waitUntilSettled()
  106. resp = self.get_url("api/tenant/tenant-one/status")
  107. self.assertIn('Content-Length', resp.headers)
  108. self.assertIn('Content-Type', resp.headers)
  109. self.assertEqual(
  110. 'application/json; charset=utf-8', resp.headers['Content-Type'])
  111. self.assertIn('Access-Control-Allow-Origin', resp.headers)
  112. self.assertIn('Cache-Control', resp.headers)
  113. self.assertIn('Last-Modified', resp.headers)
  114. self.assertTrue(resp.headers['Last-Modified'].endswith(' GMT'))
  115. self.executor_server.hold_jobs_in_build = False
  116. self.executor_server.release()
  117. self.waitUntilSettled()
  118. data = resp.json()
  119. status_jobs = []
  120. for p in data['pipelines']:
  121. for q in p['change_queues']:
  122. if p['name'] in ['gate', 'conflict']:
  123. self.assertEqual(q['window'], 20)
  124. else:
  125. self.assertEqual(q['window'], 0)
  126. for head in q['heads']:
  127. for change in head:
  128. self.assertIn(
  129. 'review.example.com/org/project',
  130. change['project_canonical'])
  131. self.assertTrue(change['active'])
  132. self.assertIn(change['id'], ('1,1', '2,1', '3,1'))
  133. for job in change['jobs']:
  134. status_jobs.append(job)
  135. self.assertEqual('project-merge', status_jobs[0]['name'])
  136. # TODO(mordred) pull uuids from self.builds
  137. self.assertEqual(
  138. 'stream/{uuid}?logfile=console.log'.format(
  139. uuid=status_jobs[0]['uuid']),
  140. status_jobs[0]['url'])
  141. self.assertEqual(
  142. 'finger://{hostname}/{uuid}'.format(
  143. hostname=self.executor_server.hostname,
  144. uuid=status_jobs[0]['uuid']),
  145. status_jobs[0]['finger_url'])
  146. # TOOD(mordred) configure a success-url on the base job
  147. self.assertEqual(
  148. 'finger://{hostname}/{uuid}'.format(
  149. hostname=self.executor_server.hostname,
  150. uuid=status_jobs[0]['uuid']),
  151. status_jobs[0]['report_url'])
  152. self.assertEqual('project-test1', status_jobs[1]['name'])
  153. self.assertEqual(
  154. 'stream/{uuid}?logfile=console.log'.format(
  155. uuid=status_jobs[1]['uuid']),
  156. status_jobs[1]['url'])
  157. self.assertEqual(
  158. 'finger://{hostname}/{uuid}'.format(
  159. hostname=self.executor_server.hostname,
  160. uuid=status_jobs[1]['uuid']),
  161. status_jobs[1]['finger_url'])
  162. self.assertEqual(
  163. 'finger://{hostname}/{uuid}'.format(
  164. hostname=self.executor_server.hostname,
  165. uuid=status_jobs[1]['uuid']),
  166. status_jobs[1]['report_url'])
  167. self.assertEqual('project-test2', status_jobs[2]['name'])
  168. self.assertEqual(
  169. 'stream/{uuid}?logfile=console.log'.format(
  170. uuid=status_jobs[2]['uuid']),
  171. status_jobs[2]['url'])
  172. self.assertEqual(
  173. 'finger://{hostname}/{uuid}'.format(
  174. hostname=self.executor_server.hostname,
  175. uuid=status_jobs[2]['uuid']),
  176. status_jobs[2]['finger_url'])
  177. self.assertEqual(
  178. 'finger://{hostname}/{uuid}'.format(
  179. hostname=self.executor_server.hostname,
  180. uuid=status_jobs[2]['uuid']),
  181. status_jobs[2]['report_url'])
  182. # check job dependencies
  183. self.assertIsNotNone(status_jobs[0]['dependencies'])
  184. self.assertIsNotNone(status_jobs[1]['dependencies'])
  185. self.assertIsNotNone(status_jobs[2]['dependencies'])
  186. self.assertEqual(len(status_jobs[0]['dependencies']), 0)
  187. self.assertEqual(len(status_jobs[1]['dependencies']), 1)
  188. self.assertEqual(len(status_jobs[2]['dependencies']), 1)
  189. self.assertIn('project-merge', status_jobs[1]['dependencies'])
  190. self.assertIn('project-merge', status_jobs[2]['dependencies'])
  191. def test_web_tenants(self):
  192. "Test that we can retrieve JSON status info"
  193. self.add_base_changes()
  194. self.executor_server.hold_jobs_in_build = True
  195. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  196. A.addApproval('Code-Review', 2)
  197. self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
  198. self.waitUntilSettled()
  199. self.executor_server.release('project-merge')
  200. self.waitUntilSettled()
  201. resp = self.get_url("api/tenants")
  202. self.assertIn('Content-Length', resp.headers)
  203. self.assertIn('Content-Type', resp.headers)
  204. self.assertEqual(
  205. 'application/json; charset=utf-8', resp.headers['Content-Type'])
  206. # self.assertIn('Access-Control-Allow-Origin', resp.headers)
  207. # self.assertIn('Cache-Control', resp.headers)
  208. # self.assertIn('Last-Modified', resp.headers)
  209. data = resp.json()
  210. self.assertEqual('tenant-one', data[0]['name'])
  211. self.assertEqual(3, data[0]['projects'])
  212. self.assertEqual(3, data[0]['queue'])
  213. # release jobs and check if the queue size is 0
  214. self.executor_server.hold_jobs_in_build = False
  215. self.executor_server.release()
  216. self.waitUntilSettled()
  217. data = self.get_url("api/tenants").json()
  218. self.assertEqual('tenant-one', data[0]['name'])
  219. self.assertEqual(3, data[0]['projects'])
  220. self.assertEqual(0, data[0]['queue'])
  221. # test that non-live items are not counted
  222. self.executor_server.hold_jobs_in_build = True
  223. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  224. B = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  225. B.setDependsOn(A, 1)
  226. self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
  227. self.waitUntilSettled()
  228. req = urllib.request.Request(
  229. "http://127.0.0.1:%s/api/tenants" % self.port)
  230. f = urllib.request.urlopen(req)
  231. data = f.read().decode('utf8')
  232. data = json.loads(data)
  233. self.assertEqual('tenant-one', data[0]['name'])
  234. self.assertEqual(3, data[0]['projects'])
  235. self.assertEqual(1, data[0]['queue'])
  236. def test_web_connections_list(self):
  237. data = self.get_url('api/connections').json()
  238. connection = {
  239. 'driver': 'gerrit',
  240. 'name': 'gerrit',
  241. 'baseurl': 'https://review.example.com',
  242. 'canonical_hostname': 'review.example.com',
  243. 'server': 'review.example.com',
  244. 'port': 29418,
  245. }
  246. self.assertEqual([connection], data)
  247. def test_web_bad_url(self):
  248. # do we redirect to index.html
  249. resp = self.get_url("status/foo")
  250. self.assertEqual(200, resp.status_code)
  251. def test_web_find_change(self):
  252. # can we filter by change id
  253. self.add_base_changes()
  254. data = self.get_url("api/tenant/tenant-one/status/change/1,1").json()
  255. self.assertEqual(1, len(data), data)
  256. self.assertEqual("org/project", data[0]['project'])
  257. data = self.get_url("api/tenant/tenant-one/status/change/2,1").json()
  258. self.assertEqual(1, len(data), data)
  259. self.assertEqual("org/project1", data[0]['project'], data)
  260. def test_web_find_job(self):
  261. # can we fetch the variants for a single job
  262. data = self.get_url('api/tenant/tenant-one/job/project-test1').json()
  263. common_config_role = {
  264. 'implicit': True,
  265. 'project_canonical_name': 'review.example.com/common-config',
  266. 'target_name': 'common-config',
  267. 'type': 'zuul',
  268. }
  269. source_ctx = {
  270. 'branch': 'master',
  271. 'path': 'zuul.yaml',
  272. 'project': 'common-config',
  273. }
  274. run = [{
  275. 'path': 'playbooks/project-test1.yaml',
  276. 'roles': [{
  277. 'implicit': True,
  278. 'project_canonical_name': 'review.example.com/common-config',
  279. 'target_name': 'common-config',
  280. 'type': 'zuul'
  281. }],
  282. 'secrets': [],
  283. 'source_context': source_ctx,
  284. }]
  285. self.assertEqual([
  286. {
  287. 'name': 'project-test1',
  288. 'abstract': False,
  289. 'ansible_version': None,
  290. 'attempts': 4,
  291. 'branches': [],
  292. 'dependencies': [],
  293. 'description': None,
  294. 'files': [],
  295. 'irrelevant_files': [],
  296. 'match_on_config_updates': True,
  297. 'final': False,
  298. 'nodeset': {
  299. 'groups': [],
  300. 'name': '',
  301. 'nodes': [{'comment': None,
  302. 'hold_job': None,
  303. 'label': 'label1',
  304. 'name': 'controller',
  305. 'aliases': [],
  306. 'state': 'unknown'}],
  307. },
  308. 'override_checkout': None,
  309. 'parent': 'base',
  310. 'post_review': None,
  311. 'protected': None,
  312. 'provides': [],
  313. 'required_projects': [],
  314. 'requires': [],
  315. 'roles': [common_config_role],
  316. 'run': run,
  317. 'pre_run': [],
  318. 'post_run': [],
  319. 'cleanup_run': [],
  320. 'semaphore': None,
  321. 'source_context': source_ctx,
  322. 'tags': [],
  323. 'timeout': None,
  324. 'variables': {},
  325. 'extra_variables': {},
  326. 'group_variables': {},
  327. 'host_variables': {},
  328. 'variant_description': '',
  329. 'voting': True
  330. }, {
  331. 'name': 'project-test1',
  332. 'abstract': False,
  333. 'ansible_version': None,
  334. 'attempts': 3,
  335. 'branches': ['stable'],
  336. 'dependencies': [],
  337. 'description': None,
  338. 'files': [],
  339. 'irrelevant_files': [],
  340. 'match_on_config_updates': True,
  341. 'final': False,
  342. 'nodeset': {
  343. 'groups': [],
  344. 'name': '',
  345. 'nodes': [{'comment': None,
  346. 'hold_job': None,
  347. 'label': 'label2',
  348. 'name': 'controller',
  349. 'aliases': [],
  350. 'state': 'unknown'}],
  351. },
  352. 'override_checkout': None,
  353. 'parent': 'base',
  354. 'post_review': None,
  355. 'protected': None,
  356. 'provides': [],
  357. 'required_projects': [],
  358. 'requires': [],
  359. 'roles': [common_config_role],
  360. 'run': run,
  361. 'pre_run': [],
  362. 'post_run': [],
  363. 'cleanup_run': [],
  364. 'semaphore': None,
  365. 'source_context': source_ctx,
  366. 'tags': [],
  367. 'timeout': None,
  368. 'variables': {},
  369. 'extra_variables': {},
  370. 'group_variables': {},
  371. 'host_variables': {},
  372. 'variant_description': 'stable',
  373. 'voting': True
  374. }], data)
  375. data = self.get_url('api/tenant/tenant-one/job/test-job').json()
  376. run[0]['path'] = 'playbooks/project-merge.yaml'
  377. self.assertEqual([
  378. {
  379. 'abstract': False,
  380. 'ansible_version': None,
  381. 'attempts': 3,
  382. 'branches': [],
  383. 'dependencies': [],
  384. 'description': None,
  385. 'files': [],
  386. 'final': False,
  387. 'irrelevant_files': [],
  388. 'match_on_config_updates': True,
  389. 'name': 'test-job',
  390. 'override_checkout': None,
  391. 'parent': 'base',
  392. 'post_review': None,
  393. 'protected': None,
  394. 'provides': [],
  395. 'required_projects': [
  396. {'override_branch': None,
  397. 'override_checkout': None,
  398. 'project_name': 'review.example.com/org/project'}],
  399. 'requires': [],
  400. 'roles': [common_config_role],
  401. 'run': run,
  402. 'pre_run': [],
  403. 'post_run': [],
  404. 'cleanup_run': [],
  405. 'semaphore': None,
  406. 'source_context': source_ctx,
  407. 'tags': [],
  408. 'timeout': None,
  409. 'variables': {},
  410. 'extra_variables': {},
  411. 'group_variables': {},
  412. 'host_variables': {},
  413. 'variant_description': '',
  414. 'voting': True
  415. }], data)
  416. def test_find_job_complete_playbooks(self):
  417. # can we fetch the variants for a single job
  418. data = self.get_url('api/tenant/tenant-one/job/complete-job').json()
  419. def expected_pb(path):
  420. return {
  421. 'path': path,
  422. 'roles': [{
  423. 'implicit': True,
  424. 'project_canonical_name':
  425. 'review.example.com/common-config',
  426. 'target_name': 'common-config',
  427. 'type': 'zuul'
  428. }],
  429. 'secrets': [],
  430. 'source_context': {
  431. 'branch': 'master',
  432. 'path': 'zuul.yaml',
  433. 'project': 'common-config',
  434. }
  435. }
  436. self.assertEqual([
  437. expected_pb("playbooks/run.yaml")
  438. ], data[0]['run'])
  439. self.assertEqual([
  440. expected_pb("playbooks/pre-run.yaml")
  441. ], data[0]['pre_run'])
  442. self.assertEqual([
  443. expected_pb("playbooks/post-run-01.yaml"),
  444. expected_pb("playbooks/post-run-02.yaml")
  445. ], data[0]['post_run'])
  446. self.assertEqual([
  447. expected_pb("playbooks/cleanup-run.yaml")
  448. ], data[0]['cleanup_run'])
  449. def test_web_nodes_list(self):
  450. # can we fetch the nodes list
  451. self.add_base_changes()
  452. data = self.get_url('api/tenant/tenant-one/nodes').json()
  453. self.assertGreater(len(data), 0)
  454. self.assertEqual("test-provider", data[0]["provider"])
  455. self.assertEqual("label1", data[0]["type"])
  456. def test_web_labels_list(self):
  457. # can we fetch the labels list
  458. data = self.get_url('api/tenant/tenant-one/labels').json()
  459. expected_list = [{'name': 'label1'}]
  460. self.assertEqual(expected_list, data)
  461. def test_web_pipeline_list(self):
  462. # can we fetch the list of pipelines
  463. data = self.get_url('api/tenant/tenant-one/pipelines').json()
  464. gerrit_trigger = {'name': 'gerrit', 'driver': 'gerrit'}
  465. timer_trigger = {'name': 'timer', 'driver': 'timer'}
  466. expected_list = [
  467. {'name': 'check', 'triggers': [gerrit_trigger]},
  468. {'name': 'gate', 'triggers': [gerrit_trigger]},
  469. {'name': 'post', 'triggers': [gerrit_trigger]},
  470. {'name': 'periodic', 'triggers': [timer_trigger]},
  471. ]
  472. self.assertEqual(expected_list, data)
  473. def test_web_project_list(self):
  474. # can we fetch the list of projects
  475. data = self.get_url('api/tenant/tenant-one/projects').json()
  476. expected_list = [
  477. {'name': 'common-config', 'type': 'config'},
  478. {'name': 'org/project', 'type': 'untrusted'},
  479. {'name': 'org/project1', 'type': 'untrusted'},
  480. {'name': 'org/project2', 'type': 'untrusted'}
  481. ]
  482. for p in expected_list:
  483. p["canonical_name"] = "review.example.com/%s" % p["name"]
  484. p["connection_name"] = "gerrit"
  485. self.assertEqual(expected_list, data)
  486. def test_web_project_get(self):
  487. # can we fetch project details
  488. data = self.get_url(
  489. 'api/tenant/tenant-one/project/org/project1').json()
  490. jobs = [[{'abstract': False,
  491. 'ansible_version': None,
  492. 'attempts': 3,
  493. 'branches': [],
  494. 'dependencies': [],
  495. 'description': None,
  496. 'files': [],
  497. 'final': False,
  498. 'irrelevant_files': [],
  499. 'match_on_config_updates': True,
  500. 'name': 'project-merge',
  501. 'override_checkout': None,
  502. 'parent': 'base',
  503. 'post_review': None,
  504. 'protected': None,
  505. 'provides': [],
  506. 'required_projects': [],
  507. 'requires': [],
  508. 'roles': [],
  509. 'run': [],
  510. 'pre_run': [],
  511. 'post_run': [],
  512. 'cleanup_run': [],
  513. 'semaphore': None,
  514. 'source_context': {
  515. 'branch': 'master',
  516. 'path': 'zuul.yaml',
  517. 'project': 'common-config'},
  518. 'tags': [],
  519. 'timeout': None,
  520. 'variables': {},
  521. 'extra_variables': {},
  522. 'group_variables': {},
  523. 'host_variables': {},
  524. 'variant_description': '',
  525. 'voting': True}],
  526. [{'abstract': False,
  527. 'ansible_version': None,
  528. 'attempts': 3,
  529. 'branches': [],
  530. 'dependencies': [{'name': 'project-merge',
  531. 'soft': False}],
  532. 'description': None,
  533. 'files': [],
  534. 'final': False,
  535. 'irrelevant_files': [],
  536. 'match_on_config_updates': True,
  537. 'name': 'project-test1',
  538. 'override_checkout': None,
  539. 'parent': 'base',
  540. 'post_review': None,
  541. 'protected': None,
  542. 'provides': [],
  543. 'required_projects': [],
  544. 'requires': [],
  545. 'roles': [],
  546. 'run': [],
  547. 'pre_run': [],
  548. 'post_run': [],
  549. 'cleanup_run': [],
  550. 'semaphore': None,
  551. 'source_context': {
  552. 'branch': 'master',
  553. 'path': 'zuul.yaml',
  554. 'project': 'common-config'},
  555. 'tags': [],
  556. 'timeout': None,
  557. 'variables': {},
  558. 'extra_variables': {},
  559. 'group_variables': {},
  560. 'host_variables': {},
  561. 'variant_description': '',
  562. 'voting': True}],
  563. [{'abstract': False,
  564. 'ansible_version': None,
  565. 'attempts': 3,
  566. 'branches': [],
  567. 'dependencies': [{'name': 'project-merge',
  568. 'soft': False}],
  569. 'description': None,
  570. 'files': [],
  571. 'final': False,
  572. 'irrelevant_files': [],
  573. 'match_on_config_updates': True,
  574. 'name': 'project-test2',
  575. 'override_checkout': None,
  576. 'parent': 'base',
  577. 'post_review': None,
  578. 'protected': None,
  579. 'provides': [],
  580. 'required_projects': [],
  581. 'requires': [],
  582. 'roles': [],
  583. 'run': [],
  584. 'pre_run': [],
  585. 'post_run': [],
  586. 'cleanup_run': [],
  587. 'semaphore': None,
  588. 'source_context': {
  589. 'branch': 'master',
  590. 'path': 'zuul.yaml',
  591. 'project': 'common-config'},
  592. 'tags': [],
  593. 'timeout': None,
  594. 'variables': {},
  595. 'extra_variables': {},
  596. 'group_variables': {},
  597. 'host_variables': {},
  598. 'variant_description': '',
  599. 'voting': True}],
  600. [{'abstract': False,
  601. 'ansible_version': None,
  602. 'attempts': 3,
  603. 'branches': [],
  604. 'dependencies': [{'name': 'project-merge',
  605. 'soft': False}],
  606. 'description': None,
  607. 'files': [],
  608. 'final': False,
  609. 'irrelevant_files': [],
  610. 'match_on_config_updates': True,
  611. 'name': 'project1-project2-integration',
  612. 'override_checkout': None,
  613. 'parent': 'base',
  614. 'post_review': None,
  615. 'protected': None,
  616. 'provides': [],
  617. 'required_projects': [],
  618. 'requires': [],
  619. 'roles': [],
  620. 'run': [],
  621. 'pre_run': [],
  622. 'post_run': [],
  623. 'cleanup_run': [],
  624. 'semaphore': None,
  625. 'source_context': {
  626. 'branch': 'master',
  627. 'path': 'zuul.yaml',
  628. 'project': 'common-config'},
  629. 'tags': [],
  630. 'timeout': None,
  631. 'variables': {},
  632. 'extra_variables': {},
  633. 'group_variables': {},
  634. 'host_variables': {},
  635. 'variant_description': '',
  636. 'voting': True}]]
  637. self.assertEqual(
  638. {
  639. 'canonical_name': 'review.example.com/org/project1',
  640. 'connection_name': 'gerrit',
  641. 'name': 'org/project1',
  642. 'configs': [{
  643. 'templates': [],
  644. 'default_branch': 'master',
  645. 'merge_mode': 'merge-resolve',
  646. 'pipelines': [{
  647. 'name': 'check',
  648. 'queue_name': None,
  649. 'jobs': jobs,
  650. }, {
  651. 'name': 'gate',
  652. 'queue_name': 'integrated',
  653. 'jobs': jobs,
  654. }, {'name': 'post',
  655. 'queue_name': None,
  656. 'jobs': [[
  657. {'abstract': False,
  658. 'ansible_version': None,
  659. 'attempts': 3,
  660. 'branches': [],
  661. 'dependencies': [],
  662. 'description': None,
  663. 'files': [],
  664. 'final': False,
  665. 'irrelevant_files': [],
  666. 'match_on_config_updates': True,
  667. 'name': 'project-post',
  668. 'override_checkout': None,
  669. 'parent': 'base',
  670. 'post_review': None,
  671. 'post_run': [],
  672. 'cleanup_run': [],
  673. 'pre_run': [],
  674. 'protected': None,
  675. 'provides': [],
  676. 'required_projects': [],
  677. 'requires': [],
  678. 'roles': [],
  679. 'run': [],
  680. 'semaphore': None,
  681. 'source_context': {'branch': 'master',
  682. 'path': 'zuul.yaml',
  683. 'project': 'common-config'},
  684. 'tags': [],
  685. 'timeout': None,
  686. 'variables': {},
  687. 'extra_variables': {},
  688. 'group_variables': {},
  689. 'host_variables': {},
  690. 'variant_description': '',
  691. 'voting': True}
  692. ]],
  693. }
  694. ]
  695. }]
  696. }, data)
  697. def test_web_keys(self):
  698. with open(os.path.join(FIXTURE_DIR, 'public.pem'), 'rb') as f:
  699. public_pem = f.read()
  700. resp = self.get_url("api/tenant/tenant-one/key/org/project.pub")
  701. self.assertEqual(resp.content, public_pem)
  702. self.assertIn('text/plain', resp.headers.get('Content-Type'))
  703. resp = self.get_url("api/tenant/non-tenant/key/org/project.pub")
  704. self.assertEqual(404, resp.status_code)
  705. resp = self.get_url("api/tenant/tenant-one/key/org/no-project.pub")
  706. self.assertEqual(404, resp.status_code)
  707. with open(os.path.join(FIXTURE_DIR, 'ssh.pub'), 'rb') as f:
  708. public_ssh = f.read()
  709. resp = self.get_url("api/tenant/tenant-one/project-ssh-key/"
  710. "org/project.pub")
  711. self.assertEqual(resp.content, public_ssh)
  712. self.assertIn('text/plain', resp.headers.get('Content-Type'))
  713. def test_web_404_on_unknown_tenant(self):
  714. resp = self.get_url("api/tenant/non-tenant/status")
  715. self.assertEqual(404, resp.status_code)
  716. def test_autohold_info_404_on_invalid_id(self):
  717. resp = self.get_url("api/tenant/tenant-one/autohold/12345")
  718. self.assertEqual(404, resp.status_code)
  719. def test_autohold_delete_404_on_invalid_id(self):
  720. resp = self.delete_url("api/tenant/tenant-one/autohold/12345")
  721. self.assertEqual(404, resp.status_code)
  722. def test_autohold_info(self):
  723. client = zuul.rpcclient.RPCClient('127.0.0.1',
  724. self.gearman_server.port)
  725. self.addCleanup(client.shutdown)
  726. r = client.autohold('tenant-one', 'org/project', 'project-test2',
  727. "", "", "reason text", 1)
  728. self.assertTrue(r)
  729. # Use autohold-list API to retrieve request ID
  730. resp = self.get_url(
  731. "api/tenant/tenant-one/autohold")
  732. self.assertEqual(200, resp.status_code, resp.text)
  733. autohold_requests = resp.json()
  734. self.assertNotEqual([], autohold_requests)
  735. self.assertEqual(1, len(autohold_requests))
  736. request_id = autohold_requests[0]['id']
  737. # Now try the autohold-info API
  738. resp = self.get_url("api/tenant/tenant-one/autohold/%s" % request_id)
  739. self.assertEqual(200, resp.status_code, resp.text)
  740. request = resp.json()
  741. self.assertEqual(request_id, request['id'])
  742. self.assertEqual('tenant-one', request['tenant'])
  743. self.assertIn('org/project', request['project'])
  744. self.assertEqual('project-test2', request['job'])
  745. self.assertEqual(".*", request['ref_filter'])
  746. self.assertEqual(1, request['max_count'])
  747. self.assertEqual(0, request['current_count'])
  748. self.assertEqual("reason text", request['reason'])
  749. self.assertEqual([], request['nodes'])
  750. def test_autohold_list(self):
  751. """test listing autoholds through zuul-web"""
  752. client = zuul.rpcclient.RPCClient('127.0.0.1',
  753. self.gearman_server.port)
  754. self.addCleanup(client.shutdown)
  755. r = client.autohold('tenant-one', 'org/project', 'project-test2',
  756. "", "", "reason text", 1)
  757. self.assertTrue(r)
  758. resp = self.get_url(
  759. "api/tenant/tenant-one/autohold")
  760. self.assertEqual(200, resp.status_code, resp.text)
  761. autohold_requests = resp.json()
  762. self.assertNotEqual([], autohold_requests)
  763. self.assertEqual(1, len(autohold_requests))
  764. ah_request = autohold_requests[0]
  765. self.assertEqual('tenant-one', ah_request['tenant'])
  766. self.assertIn('org/project', ah_request['project'])
  767. self.assertEqual('project-test2', ah_request['job'])
  768. self.assertEqual(".*", ah_request['ref_filter'])
  769. self.assertEqual(1, ah_request['max_count'])
  770. self.assertEqual(0, ah_request['current_count'])
  771. self.assertEqual("reason text", ah_request['reason'])
  772. self.assertEqual([], ah_request['nodes'])
  773. # filter by project
  774. resp = self.get_url(
  775. "api/tenant/tenant-one/autohold?project=org/project2")
  776. self.assertEqual(200, resp.status_code, resp.text)
  777. autohold_requests = resp.json()
  778. self.assertEqual([], autohold_requests)
  779. resp = self.get_url(
  780. "api/tenant/tenant-one/autohold?project=org/project")
  781. self.assertEqual(200, resp.status_code, resp.text)
  782. autohold_requests = resp.json()
  783. self.assertNotEqual([], autohold_requests)
  784. self.assertEqual(1, len(autohold_requests))
  785. ah_request = autohold_requests[0]
  786. self.assertEqual('tenant-one', ah_request['tenant'])
  787. self.assertIn('org/project', ah_request['project'])
  788. self.assertEqual('project-test2', ah_request['job'])
  789. self.assertEqual(".*", ah_request['ref_filter'])
  790. self.assertEqual(1, ah_request['max_count'])
  791. self.assertEqual(0, ah_request['current_count'])
  792. self.assertEqual("reason text", ah_request['reason'])
  793. self.assertEqual([], ah_request['nodes'])
  794. def test_admin_routes_404_by_default(self):
  795. resp = self.post_url(
  796. "api/tenant/tenant-one/project/org/project/autohold",
  797. json={'job': 'project-test1',
  798. 'count': 1,
  799. 'reason': 'because',
  800. 'node_hold_expiration': 36000})
  801. self.assertEqual(404, resp.status_code)
  802. resp = self.post_url(
  803. "api/tenant/tenant-one/project/org/project/enqueue",
  804. json={'trigger': 'gerrit',
  805. 'change': '2,1',
  806. 'pipeline': 'check'})
  807. self.assertEqual(404, resp.status_code)
  808. resp = self.post_url(
  809. "api/tenant/tenant-one/project/org/project/enqueue",
  810. json={'trigger': 'gerrit',
  811. 'ref': 'abcd',
  812. 'newrev': 'aaaa',
  813. 'oldrev': 'bbbb',
  814. 'pipeline': 'check'})
  815. self.assertEqual(404, resp.status_code)
  816. def test_jobs_list(self):
  817. jobs = self.get_url("api/tenant/tenant-one/jobs").json()
  818. self.assertEqual(len(jobs), 10)
  819. resp = self.get_url("api/tenant/non-tenant/jobs")
  820. self.assertEqual(404, resp.status_code)
  821. def test_jobs_list_variants(self):
  822. resp = self.get_url("api/tenant/tenant-one/jobs").json()
  823. for job in resp:
  824. if job['name'] in ["base", "noop"]:
  825. variants = None
  826. elif job['name'] == 'project-test1':
  827. variants = [
  828. {'parent': 'base'},
  829. {'branches': ['stable'], 'parent': 'base'},
  830. ]
  831. else:
  832. variants = [{'parent': 'base'}]
  833. self.assertEqual(variants, job.get('variants'))
  834. def test_jobs_list_tags(self):
  835. resp = self.get_url("api/tenant/tenant-one/jobs").json()
  836. post_job = None
  837. for job in resp:
  838. if job['name'] == 'project-post':
  839. post_job = job
  840. break
  841. self.assertIsNotNone(post_job)
  842. self.assertEqual(['post'], post_job.get('tags'))
  843. def test_web_job_noop(self):
  844. job = self.get_url("api/tenant/tenant-one/job/noop").json()
  845. self.assertEqual("noop", job[0]["name"])
  846. def test_freeze_jobs(self):
  847. # Test can get a list of the jobs for a given project+pipeline+branch.
  848. resp = self.get_url(
  849. "api/tenant/tenant-one/pipeline/check"
  850. "/project/org/project1/branch/master/freeze-jobs")
  851. freeze_jobs = [{
  852. 'name': 'project-merge',
  853. 'dependencies': [],
  854. }, {
  855. 'name': 'project-test1',
  856. 'dependencies': [{
  857. 'name': 'project-merge',
  858. 'soft': False,
  859. }],
  860. }, {
  861. 'name': 'project-test2',
  862. 'dependencies': [{
  863. 'name': 'project-merge',
  864. 'soft': False,
  865. }],
  866. }, {
  867. 'name': 'project1-project2-integration',
  868. 'dependencies': [{
  869. 'name': 'project-merge',
  870. 'soft': False,
  871. }],
  872. }]
  873. self.assertEqual(freeze_jobs, resp.json())
  874. def test_freeze_jobs_set_includes_all_jobs(self):
  875. # When freezing a job set we want to include all jobs even if they
  876. # have certain matcher requirements (such as required files) since we
  877. # can't otherwise evaluate them.
  878. resp = self.get_url(
  879. "api/tenant/tenant-one/pipeline/gate"
  880. "/project/org/project/branch/master/freeze-jobs")
  881. expected = {
  882. 'name': 'project-testfile',
  883. 'dependencies': [{
  884. 'name': 'project-merge',
  885. 'soft': False,
  886. }],
  887. }
  888. self.assertIn(expected, resp.json())
  889. class TestWebMultiTenant(BaseTestWeb):
  890. tenant_config_file = 'config/multi-tenant/main.yaml'
  891. def test_web_labels_allowed_list(self):
  892. labels = ["tenant-one-label", "fake", "tenant-two-label"]
  893. self.fake_nodepool.registerLauncher(labels, "FakeLauncher2")
  894. # Tenant-one has label restriction in place on tenant-two
  895. res = self.get_url('api/tenant/tenant-one/labels').json()
  896. self.assertEqual([{'name': 'fake'}, {'name': 'tenant-one-label'}], res)
  897. # Tenant-two has label restriction in place on tenant-one
  898. expected = ["label1", "fake", "tenant-two-label"]
  899. res = self.get_url('api/tenant/tenant-two/labels').json()
  900. self.assertEqual(
  901. list(map(lambda x: {'name': x}, sorted(expected))), res)
  902. class TestWebSecrets(BaseTestWeb):
  903. tenant_config_file = 'config/secrets/main.yaml'
  904. def test_web_find_job_secret(self):
  905. data = self.get_url('api/tenant/tenant-one/job/project1-secret').json()
  906. run = data[0]['run']
  907. secret = {'name': 'project1_secret', 'alias': 'secret_name'}
  908. self.assertEqual([secret], run[0]['secrets'])
  909. class TestInfo(ZuulDBTestCase, BaseTestWeb):
  910. config_file = 'zuul-sql-driver.conf'
  911. def setUp(self):
  912. super(TestInfo, self).setUp()
  913. web_config = self.config_ini_data.get('web', {})
  914. self.websocket_url = web_config.get('websocket_url')
  915. self.stats_url = web_config.get('stats_url')
  916. statsd_config = self.config_ini_data.get('statsd', {})
  917. self.stats_prefix = statsd_config.get('prefix')
  918. def _expected_info(self):
  919. return {
  920. "info": {
  921. "capabilities": {
  922. "job_history": True,
  923. "auth": {
  924. "realms": {},
  925. "default_realm": None
  926. }
  927. },
  928. "stats": {
  929. "url": self.stats_url,
  930. "prefix": self.stats_prefix,
  931. "type": "graphite",
  932. },
  933. "websocket_url": self.websocket_url,
  934. }
  935. }
  936. def test_info(self):
  937. info = self.get_url("api/info").json()
  938. self.assertEqual(
  939. info, self._expected_info())
  940. def test_tenant_info(self):
  941. info = self.get_url("api/tenant/tenant-one/info").json()
  942. expected_info = self._expected_info()
  943. expected_info['info']['tenant'] = 'tenant-one'
  944. self.assertEqual(
  945. info, expected_info)
  946. class TestWebCapabilitiesInfo(TestInfo):
  947. config_file = 'zuul-admin-web-oidc.conf'
  948. def _expected_info(self):
  949. info = super(TestWebCapabilitiesInfo, self)._expected_info()
  950. info['info']['capabilities']['auth'] = {
  951. 'realms': {
  952. 'myOIDC1': {
  953. 'authority': 'http://oidc1',
  954. 'client_id': 'zuul',
  955. 'type': 'JWT',
  956. 'scope': 'openid profile',
  957. 'driver': 'OpenIDConnect',
  958. },
  959. 'myOIDC2': {
  960. 'authority': 'http://oidc2',
  961. 'client_id': 'zuul',
  962. 'type': 'JWT',
  963. 'scope': 'openid profile email special-scope',
  964. 'driver': 'OpenIDConnect',
  965. },
  966. 'zuul.example.com': {
  967. 'authority': 'zuul_operator',
  968. 'client_id': 'zuul.example.com',
  969. 'type': 'JWT',
  970. 'driver': 'HS256',
  971. }
  972. },
  973. 'default_realm': 'myOIDC1'
  974. }
  975. return info
  976. class TestTenantInfoConfigBroken(BaseTestWeb):
  977. tenant_config_file = 'config/broken/main.yaml'
  978. def test_tenant_info_broken_config(self):
  979. config_errors = self.get_url(
  980. "api/tenant/tenant-one/config-errors").json()
  981. self.assertEqual(
  982. len(config_errors), 2)
  983. self.assertEqual(
  984. config_errors[0]['source_context']['project'], 'org/project3')
  985. self.assertIn('Zuul encountered an error while accessing the repo '
  986. 'org/project3',
  987. config_errors[0]['error'])
  988. self.assertEqual(
  989. config_errors[1]['source_context']['project'], 'org/project2')
  990. self.assertEqual(
  991. config_errors[1]['source_context']['branch'], 'master')
  992. self.assertEqual(
  993. config_errors[1]['source_context']['path'], '.zuul.yaml')
  994. self.assertIn('Zuul encountered a syntax error',
  995. config_errors[1]['error'])
  996. resp = self.get_url("api/tenant/non-tenant/config-errors")
  997. self.assertEqual(404, resp.status_code)
  998. class TestWebSocketInfo(TestInfo):
  999. config_ini_data = {
  1000. 'web': {
  1001. 'websocket_url': 'wss://ws.example.com'
  1002. }
  1003. }
  1004. class TestGraphiteUrl(TestInfo):
  1005. config_ini_data = {
  1006. 'statsd': {
  1007. 'prefix': 'example'
  1008. },
  1009. 'web': {
  1010. 'stats_url': 'https://graphite.example.com',
  1011. }
  1012. }
  1013. class TestBuildInfo(ZuulDBTestCase, BaseTestWeb):
  1014. config_file = 'zuul-sql-driver.conf'
  1015. tenant_config_file = 'config/sql-driver/main.yaml'
  1016. def test_web_list_builds(self):
  1017. # Generate some build records in the db.
  1018. self.add_base_changes()
  1019. self.executor_server.hold_jobs_in_build = False
  1020. self.executor_server.release()
  1021. self.waitUntilSettled()
  1022. builds = self.get_url("api/tenant/tenant-one/builds").json()
  1023. self.assertEqual(len(builds), 6)
  1024. uuid = builds[0]['uuid']
  1025. build = self.get_url("api/tenant/tenant-one/build/%s" % uuid).json()
  1026. self.assertEqual(build['job_name'], builds[0]['job_name'])
  1027. resp = self.get_url("api/tenant/tenant-one/build/1234")
  1028. self.assertEqual(404, resp.status_code)
  1029. builds_query = self.get_url("api/tenant/tenant-one/builds?"
  1030. "project=org/project&"
  1031. "project=org/project1").json()
  1032. self.assertEqual(len(builds_query), 6)
  1033. resp = self.get_url("api/tenant/non-tenant/builds")
  1034. self.assertEqual(404, resp.status_code)
  1035. def test_web_badge(self):
  1036. # Generate some build records in the db.
  1037. self.add_base_changes()
  1038. self.executor_server.hold_jobs_in_build = False
  1039. self.executor_server.release()
  1040. self.waitUntilSettled()
  1041. # Now request badge for the buildsets
  1042. result = self.get_url("api/tenant/tenant-one/badge")
  1043. self.log.error(result.content)
  1044. result.raise_for_status()
  1045. self.assertTrue(result.text.startswith('<svg '))
  1046. self.assertIn('passing', result.text)
  1047. # Generate a failing record
  1048. self.executor_server.hold_jobs_in_build = True
  1049. C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
  1050. C.addApproval('Code-Review', 2)
  1051. self.fake_gerrit.addEvent(C.addApproval('Approved', 1))
  1052. self.waitUntilSettled()
  1053. self.executor_server.failJob('project-merge', C)
  1054. self.executor_server.hold_jobs_in_build = False
  1055. self.executor_server.release()
  1056. self.waitUntilSettled()
  1057. # Request again badge for the buildsets
  1058. result = self.get_url("api/tenant/tenant-one/badge")
  1059. self.log.error(result.content)
  1060. result.raise_for_status()
  1061. self.assertTrue(result.text.startswith('<svg '))
  1062. self.assertIn('failing', result.text)
  1063. def test_web_list_buildsets(self):
  1064. # Generate some build records in the db.
  1065. self.add_base_changes()
  1066. self.executor_server.hold_jobs_in_build = False
  1067. self.executor_server.release()
  1068. self.waitUntilSettled()
  1069. buildsets = self.get_url("api/tenant/tenant-one/buildsets").json()
  1070. self.assertEqual(2, len(buildsets))
  1071. project_bs = [x for x in buildsets if x["project"] == "org/project"][0]
  1072. buildset = self.get_url(
  1073. "api/tenant/tenant-one/buildset/%s" % project_bs['uuid']).json()
  1074. self.assertEqual(3, len(buildset["builds"]))
  1075. project_test1_build = [x for x in buildset["builds"]
  1076. if x["job_name"] == "project-test1"][0]
  1077. self.assertEqual('SUCCESS', project_test1_build['result'])
  1078. project_test2_build = [x for x in buildset["builds"]
  1079. if x["job_name"] == "project-test2"][0]
  1080. self.assertEqual('SUCCESS', project_test2_build['result'])
  1081. project_merge_build = [x for x in buildset["builds"]
  1082. if x["job_name"] == "project-merge"][0]
  1083. self.assertEqual('SUCCESS', project_merge_build['result'])
  1084. @simple_layout('layouts/sql-build-error.yaml')
  1085. def test_build_error(self):
  1086. conf = textwrap.dedent(
  1087. """
  1088. - job:
  1089. name: test-job
  1090. run: playbooks/dne.yaml
  1091. - project:
  1092. name: org/project
  1093. check:
  1094. jobs:
  1095. - test-job
  1096. """)
  1097. file_dict = {'.zuul.yaml': conf}
  1098. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
  1099. files=file_dict)
  1100. self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
  1101. self.waitUntilSettled()
  1102. builds = self.get_url("api/tenant/tenant-one/builds").json()
  1103. self.assertIn('Unable to find playbook',
  1104. builds[0]['error_detail'])
  1105. class TestArtifacts(ZuulDBTestCase, BaseTestWeb, AnsibleZuulTestCase):
  1106. config_file = 'zuul-sql-driver.conf'
  1107. tenant_config_file = 'config/sql-driver/main.yaml'
  1108. def test_artifacts(self):
  1109. # Generate some build records in the db.
  1110. self.add_base_changes()
  1111. self.executor_server.hold_jobs_in_build = False
  1112. self.executor_server.release()
  1113. self.waitUntilSettled()
  1114. build_query = self.get_url("api/tenant/tenant-one/builds?"
  1115. "project=org/project&"
  1116. "job_name=project-test1").json()
  1117. self.assertEqual(len(build_query), 1)
  1118. self.assertEqual(len(build_query[0]['artifacts']), 3)
  1119. arts = build_query[0]['artifacts']
  1120. arts.sort(key=lambda x: x['name'])
  1121. self.assertEqual(build_query[0]['artifacts'], [
  1122. {'url': 'http://example.com/docs',
  1123. 'name': 'docs'},
  1124. {'url': 'http://logs.example.com/build/relative/docs',
  1125. 'name': 'relative',
  1126. 'metadata': {'foo': 'bar'}},
  1127. {'url': 'http://example.com/tarball',
  1128. 'name': 'tarball'},
  1129. ])
  1130. def test_buildset_artifacts(self):
  1131. self.add_base_changes()
  1132. self.executor_server.hold_jobs_in_build = False
  1133. self.executor_server.release()
  1134. self.waitUntilSettled()
  1135. buildsets = self.get_url("api/tenant/tenant-one/buildsets").json()
  1136. project_bs = [x for x in buildsets if x["project"] == "org/project"][0]
  1137. buildset = self.get_url(
  1138. "api/tenant/tenant-one/buildset/%s" % project_bs['uuid']).json()
  1139. self.assertEqual(3, len(buildset["builds"]))
  1140. test1_build = [x for x in buildset["builds"]
  1141. if x["job_name"] == "project-test1"][0]
  1142. arts = test1_build['artifacts']
  1143. arts.sort(key=lambda x: x['name'])
  1144. self.assertEqual([
  1145. {'url': 'http://example.com/docs',
  1146. 'name': 'docs'},
  1147. {'url': 'http://logs.example.com/build/relative/docs',
  1148. 'name': 'relative',
  1149. 'metadata': {'foo': 'bar'}},
  1150. {'url': 'http://example.com/tarball',
  1151. 'name': 'tarball'},
  1152. ], test1_build['artifacts'])
  1153. class TestTenantScopedWebApi(BaseTestWeb):
  1154. config_file = 'zuul-admin-web.conf'
  1155. def test_admin_routes_no_token(self):
  1156. resp = self.post_url(
  1157. "api/tenant/tenant-one/project/org/project/autohold",
  1158. json={'job': 'project-test1',
  1159. 'count': 1,
  1160. 'reason': 'because',
  1161. 'node_hold_expiration': 36000})
  1162. self.assertEqual(401, resp.status_code)
  1163. resp = self.post_url(
  1164. "api/tenant/tenant-one/project/org/project/enqueue",
  1165. json={'trigger': 'gerrit',
  1166. 'change': '2,1',
  1167. 'pipeline': 'check'})
  1168. self.assertEqual(401, resp.status_code)
  1169. resp = self.post_url(
  1170. "api/tenant/tenant-one/project/org/project/enqueue",
  1171. json={'trigger': 'gerrit',
  1172. 'ref': 'abcd',
  1173. 'newrev': 'aaaa',
  1174. 'oldrev': 'bbbb',
  1175. 'pipeline': 'check'})
  1176. self.assertEqual(401, resp.status_code)
  1177. def test_bad_key_JWT_token(self):
  1178. authz = {'iss': 'zuul_operator',
  1179. 'aud': 'zuul.example.com',
  1180. 'sub': 'testuser',
  1181. 'zuul': {
  1182. 'admin': ['tenant-one', ],
  1183. },
  1184. 'exp': time.time() + 3600}
  1185. token = jwt.encode(authz, key='OnlyZuulNoDana',
  1186. algorithm='HS256').decode('utf-8')
  1187. resp = self.post_url(
  1188. "api/tenant/tenant-one/project/org/project/autohold",
  1189. headers={'Authorization': 'Bearer %s' % token},
  1190. json={'job': 'project-test1',
  1191. 'count': 1,
  1192. 'reason': 'because',
  1193. 'node_hold_expiration': 36000})
  1194. self.assertEqual(401, resp.status_code)
  1195. resp = self.post_url(
  1196. "api/tenant/tenant-one/project/org/project/enqueue",
  1197. headers={'Authorization': 'Bearer %s' % token},
  1198. json={'trigger': 'gerrit',
  1199. 'change': '2,1',
  1200. 'pipeline': 'check'})
  1201. self.assertEqual(401, resp.status_code)
  1202. resp = self.post_url(
  1203. "api/tenant/tenant-one/project/org/project/enqueue",
  1204. headers={'Authorization': 'Bearer %s' % token},
  1205. json={'trigger': 'gerrit',
  1206. 'ref': 'abcd',
  1207. 'newrev': 'aaaa',
  1208. 'oldrev': 'bbbb',
  1209. 'pipeline': 'check'})
  1210. self.assertEqual(401, resp.status_code)
  1211. def test_expired_JWT_token(self):
  1212. authz = {'iss': 'zuul_operator',
  1213. 'sub': 'testuser',
  1214. 'aud': 'zuul.example.com',
  1215. 'zuul': {
  1216. 'admin': ['tenant-one', ]
  1217. },
  1218. 'exp': time.time() - 3600}
  1219. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1220. algorithm='HS256').decode('utf-8')
  1221. resp = self.post_url(
  1222. "api/tenant/tenant-one/project/org/project/autohold",
  1223. headers={'Authorization': 'Bearer %s' % token},
  1224. json={'job': 'project-test1',
  1225. 'count': 1,
  1226. 'reason': 'because',
  1227. 'node_hold_expiration': 36000})
  1228. self.assertEqual(401, resp.status_code)
  1229. resp = self.post_url(
  1230. "api/tenant/tenant-one/project/org/project/enqueue",
  1231. headers={'Authorization': 'Bearer %s' % token},
  1232. json={'trigger': 'gerrit',
  1233. 'change': '2,1',
  1234. 'pipeline': 'check'})
  1235. self.assertEqual(401, resp.status_code)
  1236. resp = self.post_url(
  1237. "api/tenant/tenant-one/project/org/project/enqueue",
  1238. headers={'Authorization': 'Bearer %s' % token},
  1239. json={'trigger': 'gerrit',
  1240. 'ref': 'abcd',
  1241. 'newrev': 'aaaa',
  1242. 'oldrev': 'bbbb',
  1243. 'pipeline': 'check'})
  1244. self.assertEqual(401, resp.status_code)
  1245. def test_valid_JWT_bad_tenants(self):
  1246. authz = {'iss': 'zuul_operator',
  1247. 'sub': 'testuser',
  1248. 'aud': 'zuul.example.com',
  1249. 'zuul': {
  1250. 'admin': ['tenant-six', 'tenant-ten', ]
  1251. },
  1252. 'exp': time.time() + 3600}
  1253. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1254. algorithm='HS256').decode('utf-8')
  1255. resp = self.post_url(
  1256. "api/tenant/tenant-one/project/org/project/autohold",
  1257. headers={'Authorization': 'Bearer %s' % token},
  1258. json={'job': 'project-test1',
  1259. 'count': 1,
  1260. 'reason': 'because',
  1261. 'node_hold_expiration': 36000})
  1262. self.assertEqual(403, resp.status_code)
  1263. resp = self.post_url(
  1264. "api/tenant/tenant-one/project/org/project/enqueue",
  1265. headers={'Authorization': 'Bearer %s' % token},
  1266. json={'trigger': 'gerrit',
  1267. 'change': '2,1',
  1268. 'pipeline': 'check'})
  1269. self.assertEqual(403, resp.status_code)
  1270. resp = self.post_url(
  1271. "api/tenant/tenant-one/project/org/project/enqueue",
  1272. headers={'Authorization': 'Bearer %s' % token},
  1273. json={'trigger': 'gerrit',
  1274. 'ref': 'abcd',
  1275. 'newrev': 'aaaa',
  1276. 'oldrev': 'bbbb',
  1277. 'pipeline': 'check'})
  1278. self.assertEqual(403, resp.status_code)
  1279. # For autohold-delete, we first must make sure that an autohold
  1280. # exists before the delete attempt.
  1281. good_authz = {'iss': 'zuul_operator',
  1282. 'aud': 'zuul.example.com',
  1283. 'sub': 'testuser',
  1284. 'zuul': {'admin': ['tenant-one', ]},
  1285. 'exp': time.time() + 3600}
  1286. args = {"reason": "some reason",
  1287. "count": 1,
  1288. 'job': 'project-test2',
  1289. 'change': None,
  1290. 'ref': None,
  1291. 'node_hold_expiration': None}
  1292. good_token = jwt.encode(good_authz, key='NoDanaOnlyZuul',
  1293. algorithm='HS256').decode('utf-8')
  1294. req = self.post_url(
  1295. 'api/tenant/tenant-one/project/org/project/autohold',
  1296. headers={'Authorization': 'Bearer %s' % good_token},
  1297. json=args)
  1298. self.assertEqual(200, req.status_code, req.text)
  1299. client = zuul.rpcclient.RPCClient('127.0.0.1',
  1300. self.gearman_server.port)
  1301. self.addCleanup(client.shutdown)
  1302. autohold_requests = client.autohold_list()
  1303. self.assertNotEqual([], autohold_requests)
  1304. self.assertEqual(1, len(autohold_requests))
  1305. request = autohold_requests[0]
  1306. resp = self.delete_url(
  1307. "api/tenant/tenant-one/autohold/%s" % request['id'],
  1308. headers={'Authorization': 'Bearer %s' % token})
  1309. self.assertEqual(403, resp.status_code)
  1310. def test_autohold(self):
  1311. """Test that autohold can be set through the admin web interface"""
  1312. args = {"reason": "some reason",
  1313. "count": 1,
  1314. 'job': 'project-test2',
  1315. 'change': None,
  1316. 'ref': None,
  1317. 'node_hold_expiration': None}
  1318. authz = {'iss': 'zuul_operator',
  1319. 'aud': 'zuul.example.com',
  1320. 'sub': 'testuser',
  1321. 'zuul': {
  1322. 'admin': ['tenant-one', ]
  1323. },
  1324. 'exp': time.time() + 3600}
  1325. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1326. algorithm='HS256').decode('utf-8')
  1327. req = self.post_url(
  1328. 'api/tenant/tenant-one/project/org/project/autohold',
  1329. headers={'Authorization': 'Bearer %s' % token},
  1330. json=args)
  1331. self.assertEqual(200, req.status_code, req.text)
  1332. data = req.json()
  1333. self.assertEqual(True, data)
  1334. # Check result in rpc client
  1335. client = zuul.rpcclient.RPCClient('127.0.0.1',
  1336. self.gearman_server.port)
  1337. self.addCleanup(client.shutdown)
  1338. autohold_requests = client.autohold_list()
  1339. self.assertNotEqual([], autohold_requests)
  1340. self.assertEqual(1, len(autohold_requests))
  1341. request = autohold_requests[0]
  1342. self.assertEqual('tenant-one', request['tenant'])
  1343. self.assertIn('org/project', request['project'])
  1344. self.assertEqual('project-test2', request['job'])
  1345. self.assertEqual(".*", request['ref_filter'])
  1346. self.assertEqual("some reason", request['reason'])
  1347. self.assertEqual(1, request['max_count'])
  1348. def test_autohold_delete(self):
  1349. authz = {'iss': 'zuul_operator',
  1350. 'aud': 'zuul.example.com',
  1351. 'sub': 'testuser',
  1352. 'zuul': {
  1353. 'admin': ['tenant-one', ]
  1354. },
  1355. 'exp': time.time() + 3600}
  1356. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1357. algorithm='HS256').decode('utf-8')
  1358. client = zuul.rpcclient.RPCClient('127.0.0.1',
  1359. self.gearman_server.port)
  1360. self.addCleanup(client.shutdown)
  1361. r = client.autohold('tenant-one', 'org/project', 'project-test2',
  1362. "", "", "reason text", 1)
  1363. self.assertTrue(r)
  1364. # Use autohold-list API to retrieve request ID
  1365. resp = self.get_url(
  1366. "api/tenant/tenant-one/autohold")
  1367. self.assertEqual(200, resp.status_code, resp.text)
  1368. autohold_requests = resp.json()
  1369. self.assertNotEqual([], autohold_requests)
  1370. self.assertEqual(1, len(autohold_requests))
  1371. request_id = autohold_requests[0]['id']
  1372. # now try the autohold-delete API
  1373. resp = self.delete_url(
  1374. "api/tenant/tenant-one/autohold/%s" % request_id,
  1375. headers={'Authorization': 'Bearer %s' % token})
  1376. self.assertEqual(204, resp.status_code, resp.text)
  1377. # autohold-list should be empty now
  1378. resp = self.get_url(
  1379. "api/tenant/tenant-one/autohold")
  1380. self.assertEqual(200, resp.status_code, resp.text)
  1381. autohold_requests = resp.json()
  1382. self.assertEqual([], autohold_requests)
  1383. def test_enqueue(self):
  1384. """Test that the admin web interface can enqueue a change"""
  1385. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  1386. A.addApproval('Code-Review', 2)
  1387. A.addApproval('Approved', 1)
  1388. authz = {'iss': 'zuul_operator',
  1389. 'aud': 'zuul.example.com',
  1390. 'sub': 'testuser',
  1391. 'zuul': {
  1392. 'admin': ['tenant-one', ]
  1393. },
  1394. 'exp': time.time() + 3600}
  1395. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1396. algorithm='HS256').decode('utf-8')
  1397. path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
  1398. enqueue_args = {'tenant': 'tenant-one',
  1399. 'project': 'org/project', }
  1400. change = {'trigger': 'gerrit',
  1401. 'change': '1,1',
  1402. 'pipeline': 'gate', }
  1403. req = self.post_url(path % enqueue_args,
  1404. headers={'Authorization': 'Bearer %s' % token},
  1405. json=change)
  1406. # The JSON returned is the same as the client's output
  1407. self.assertEqual(200, req.status_code, req.text)
  1408. data = req.json()
  1409. self.assertEqual(True, data)
  1410. self.waitUntilSettled()
  1411. def test_enqueue_ref(self):
  1412. """Test that the admin web interface can enqueue a ref"""
  1413. p = "review.example.com/org/project"
  1414. upstream = self.getUpstreamRepos([p])
  1415. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  1416. A.setMerged()
  1417. A_commit = str(upstream[p].commit('master'))
  1418. self.log.debug("A commit: %s" % A_commit)
  1419. path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
  1420. enqueue_args = {'tenant': 'tenant-one',
  1421. 'project': 'org/project', }
  1422. ref = {'trigger': 'gerrit',
  1423. 'ref': 'master',
  1424. 'oldrev': '90f173846e3af9154517b88543ffbd1691f31366',
  1425. 'newrev': A_commit,
  1426. 'pipeline': 'post', }
  1427. authz = {'iss': 'zuul_operator',
  1428. 'aud': 'zuul.example.com',
  1429. 'sub': 'testuser',
  1430. 'zuul': {
  1431. 'admin': ['tenant-one', ]
  1432. },
  1433. 'exp': time.time() + 3600}
  1434. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1435. algorithm='HS256').decode('utf-8')
  1436. req = self.post_url(path % enqueue_args,
  1437. headers={'Authorization': 'Bearer %s' % token},
  1438. json=ref)
  1439. self.assertEqual(200, req.status_code, req.text)
  1440. # The JSON returned is the same as the client's output
  1441. data = req.json()
  1442. self.assertEqual(True, data)
  1443. self.waitUntilSettled()
  1444. def test_dequeue(self):
  1445. """Test that the admin web interface can dequeue a change"""
  1446. start_builds = len(self.builds)
  1447. self.create_branch('org/project', 'stable')
  1448. self.executor_server.hold_jobs_in_build = True
  1449. self.commitConfigUpdate('common-config', 'layouts/timer.yaml')
  1450. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  1451. self.waitUntilSettled()
  1452. for _ in iterate_timeout(30, 'Wait for a build on hold'):
  1453. if len(self.builds) > start_builds:
  1454. break
  1455. self.waitUntilSettled()
  1456. authz = {'iss': 'zuul_operator',
  1457. 'aud': 'zuul.example.com',
  1458. 'sub': 'testuser',
  1459. 'zuul': {
  1460. 'admin': ['tenant-one', ]
  1461. },
  1462. 'exp': time.time() + 3600}
  1463. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1464. algorithm='HS256').decode('utf-8')
  1465. path = "api/tenant/%(tenant)s/project/%(project)s/dequeue"
  1466. dequeue_args = {'tenant': 'tenant-one',
  1467. 'project': 'org/project', }
  1468. change = {'ref': 'refs/heads/stable',
  1469. 'pipeline': 'periodic', }
  1470. req = self.post_url(path % dequeue_args,
  1471. headers={'Authorization': 'Bearer %s' % token},
  1472. json=change)
  1473. # The JSON returned is the same as the client's output
  1474. self.assertEqual(200, req.status_code, req.text)
  1475. data = req.json()
  1476. self.assertEqual(True, data)
  1477. self.waitUntilSettled()
  1478. self.commitConfigUpdate('common-config',
  1479. 'layouts/no-timer.yaml')
  1480. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  1481. self.waitUntilSettled()
  1482. self.executor_server.hold_jobs_in_build = False
  1483. self.executor_server.release()
  1484. self.waitUntilSettled()
  1485. self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 1)
  1486. def test_OPTIONS(self):
  1487. """Ensure that protected endpoints handle CORS preflight requests
  1488. properly"""
  1489. # Note that %tenant, %project are not relevant here. The client is
  1490. # just checking what the endpoint allows.
  1491. endpoints = [
  1492. {'action': 'enqueue',
  1493. 'path': 'api/tenant/my-tenant/project/my-project/enqueue',
  1494. 'allowed_methods': ['POST', ]},
  1495. {'action': 'enqueue_ref',
  1496. 'path': 'api/tenant/my-tenant/project/my-project/enqueue',
  1497. 'allowed_methods': ['POST', ]},
  1498. {'action': 'autohold',
  1499. 'path': 'api/tenant/my-tenant/project/my-project/autohold',
  1500. 'allowed_methods': ['GET', 'POST', ]},
  1501. {'action': 'autohold_by_request_id',
  1502. 'path': 'api/tenant/my-tenant/autohold/123',
  1503. 'allowed_methods': ['GET', 'DELETE', ]},
  1504. {'action': 'authorizations',
  1505. 'path': 'api/user/authorizations',
  1506. 'allowed_methods': ['GET', ]},
  1507. {'action': 'dequeue',
  1508. 'path': 'api/tenant/my-tenant/project/my-project/enqueue',
  1509. 'allowed_methods': ['POST', ]},
  1510. ]
  1511. for endpoint in endpoints:
  1512. preflight = self.options_url(
  1513. endpoint['path'],
  1514. headers={'Access-Control-Request-Method': 'GET',
  1515. 'Access-Control-Request-Headers': 'Authorization'})
  1516. self.assertEqual(
  1517. 204,
  1518. preflight.status_code,
  1519. "%s failed: %s" % (endpoint['action'], preflight.text))
  1520. self.assertEqual(
  1521. '*',
  1522. preflight.headers.get('Access-Control-Allow-Origin'),
  1523. "%s failed: %s" % (endpoint['action'], preflight.headers))
  1524. self.assertEqual(
  1525. 'Authorization, Content-Type',
  1526. preflight.headers.get('Access-Control-Allow-Headers'),
  1527. "%s failed: %s" % (endpoint['action'], preflight.headers))
  1528. allowed_methods = preflight.headers.get(
  1529. 'Access-Control-Allow-Methods').split(', ')
  1530. self.assertTrue(
  1531. 'OPTIONS' in allowed_methods,
  1532. "%s has allowed methods: %s" % (endpoint['action'],
  1533. allowed_methods))
  1534. for method in endpoint['allowed_methods']:
  1535. self.assertTrue(
  1536. method in allowed_methods,
  1537. "%s has allowed methods: %s,"
  1538. " expected: %s" % (endpoint['action'],
  1539. allowed_methods,
  1540. endpoint['allowed_methods']))
  1541. class TestTenantScopedWebApiWithAuthRules(BaseTestWeb):
  1542. config_file = 'zuul-admin-web-no-override.conf'
  1543. tenant_config_file = 'config/authorization/single-tenant/main.yaml'
  1544. def test_override_not_allowed(self):
  1545. """Test that authz cannot be overriden if config does not allow it"""
  1546. args = {"reason": "some reason",
  1547. "count": 1,
  1548. 'job': 'project-test2',
  1549. 'change': None,
  1550. 'ref': None,
  1551. 'node_hold_expiration': None}
  1552. authz = {'iss': 'zuul_operator',
  1553. 'aud': 'zuul.example.com',
  1554. 'sub': 'testuser',
  1555. 'zuul': {
  1556. 'admin': ['tenant-one', ],
  1557. },
  1558. 'exp': time.time() + 3600}
  1559. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1560. algorithm='HS256').decode('utf-8')
  1561. req = self.post_url(
  1562. 'api/tenant/tenant-one/project/org/project/autohold',
  1563. headers={'Authorization': 'Bearer %s' % token},
  1564. json=args)
  1565. self.assertEqual(401, req.status_code, req.text)
  1566. def test_tenant_level_rule(self):
  1567. """Test that authz rules defined at tenant level are checked"""
  1568. path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
  1569. def _test_project_enqueue_with_authz(i, project, authz, expected):
  1570. f_ch = self.fake_gerrit.addFakeChange(project, 'master',
  1571. '%s %i' % (project, i))
  1572. f_ch.addApproval('Code-Review', 2)
  1573. f_ch.addApproval('Approved', 1)
  1574. change = {'trigger': 'gerrit',
  1575. 'change': '%i,1' % i,
  1576. 'pipeline': 'gate', }
  1577. enqueue_args = {'tenant': 'tenant-one',
  1578. 'project': project, }
  1579. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1580. algorithm='HS256').decode('utf-8')
  1581. req = self.post_url(path % enqueue_args,
  1582. headers={'Authorization': 'Bearer %s' % token},
  1583. json=change)
  1584. self.assertEqual(expected, req.status_code, req.text)
  1585. self.waitUntilSettled()
  1586. i = 0
  1587. for p in ['org/project', 'org/project1', 'org/project2']:
  1588. i += 1
  1589. # Authorized sub
  1590. authz = {'iss': 'zuul_operator',
  1591. 'aud': 'zuul.example.com',
  1592. 'sub': 'venkman',
  1593. 'exp': time.time() + 3600}
  1594. _test_project_enqueue_with_authz(i, p, authz, 200)
  1595. i += 1
  1596. # Unauthorized sub
  1597. authz = {'iss': 'zuul_operator',
  1598. 'aud': 'zuul.example.com',
  1599. 'sub': 'vigo',
  1600. 'exp': time.time() + 3600}
  1601. _test_project_enqueue_with_authz(i, p, authz, 403)
  1602. i += 1
  1603. # unauthorized issuer
  1604. authz = {'iss': 'columbia.edu',
  1605. 'aud': 'zuul.example.com',
  1606. 'sub': 'stantz',
  1607. 'exp': time.time() + 3600}
  1608. _test_project_enqueue_with_authz(i, p, authz, 401)
  1609. self.waitUntilSettled()
  1610. def test_group_rule(self):
  1611. """Test a group rule"""
  1612. A = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A')
  1613. A.addApproval('Code-Review', 2)
  1614. A.addApproval('Approved', 1)
  1615. authz = {'iss': 'zuul_operator',
  1616. 'aud': 'zuul.example.com',
  1617. 'sub': 'melnitz',
  1618. 'groups': ['ghostbusters', 'secretary'],
  1619. 'exp': time.time() + 3600}
  1620. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1621. algorithm='HS256').decode('utf-8')
  1622. path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
  1623. enqueue_args = {'tenant': 'tenant-one',
  1624. 'project': 'org/project2', }
  1625. change = {'trigger': 'gerrit',
  1626. 'change': '1,1',
  1627. 'pipeline': 'gate', }
  1628. req = self.post_url(path % enqueue_args,
  1629. headers={'Authorization': 'Bearer %s' % token},
  1630. json=change)
  1631. self.assertEqual(200, req.status_code, req.text)
  1632. self.waitUntilSettled()
  1633. def test_depth_claim_rule(self):
  1634. """Test a rule based on a complex claim"""
  1635. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  1636. A.addApproval('Code-Review', 2)
  1637. A.addApproval('Approved', 1)
  1638. authz = {'iss': 'zuul_operator',
  1639. 'aud': 'zuul.example.com',
  1640. 'sub': 'zeddemore',
  1641. 'vehicle': {
  1642. 'car': 'ecto-1'},
  1643. 'exp': time.time() + 3600}
  1644. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1645. algorithm='HS256').decode('utf-8')
  1646. path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
  1647. enqueue_args = {'tenant': 'tenant-one',
  1648. 'project': 'org/project', }
  1649. change = {'trigger': 'gerrit',
  1650. 'change': '1,1',
  1651. 'pipeline': 'gate', }
  1652. req = self.post_url(path % enqueue_args,
  1653. headers={'Authorization': 'Bearer %s' % token},
  1654. json=change)
  1655. self.assertEqual(200, req.status_code, req.text)
  1656. self.waitUntilSettled()
  1657. def test_user_actions_action_override(self):
  1658. """Test that user with 'zuul.admin' claim does NOT get it back"""
  1659. admin_tenants = ['tenant-zero', ]
  1660. authz = {'iss': 'zuul_operator',
  1661. 'aud': 'zuul.example.com',
  1662. 'sub': 'testuser',
  1663. 'zuul': {'admin': admin_tenants},
  1664. 'exp': time.time() + 3600}
  1665. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1666. algorithm='HS256').decode('utf-8')
  1667. req = self.get_url('/api/user/authorizations',
  1668. headers={'Authorization': 'Bearer %s' % token})
  1669. self.assertEqual(401, req.status_code, req.text)
  1670. def test_user_actions(self):
  1671. """Test that users get the right 'zuul.actions' trees"""
  1672. users = [
  1673. {'authz': {'iss': 'zuul_operator',
  1674. 'aud': 'zuul.example.com',
  1675. 'sub': 'vigo'},
  1676. 'zuul.admin': []},
  1677. {'authz': {'iss': 'zuul_operator',
  1678. 'aud': 'zuul.example.com',
  1679. 'sub': 'venkman'},
  1680. 'zuul.admin': ['tenant-one', ]},
  1681. {'authz': {'iss': 'zuul_operator',
  1682. 'aud': 'zuul.example.com',
  1683. 'sub': 'stantz'},
  1684. 'zuul.admin': []},
  1685. {'authz': {'iss': 'zuul_operator',
  1686. 'aud': 'zuul.example.com',
  1687. 'sub': 'zeddemore',
  1688. 'vehicle': {
  1689. 'car': 'ecto-1'
  1690. }},
  1691. 'zuul.admin': ['tenant-one', ]},
  1692. {'authz': {'iss': 'zuul_operator',
  1693. 'aud': 'zuul.example.com',
  1694. 'sub': 'melnitz',
  1695. 'groups': ['secretary', 'ghostbusters']},
  1696. 'zuul.admin': ['tenant-one', ]},
  1697. ]
  1698. for test_user in users:
  1699. authz = test_user['authz']
  1700. authz['exp'] = time.time() + 3600
  1701. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1702. algorithm='HS256').decode('utf-8')
  1703. req = self.get_url('/api/user/authorizations',
  1704. headers={'Authorization': 'Bearer %s' % token})
  1705. self.assertEqual(200, req.status_code, req.text)
  1706. data = req.json()
  1707. self.assertTrue('zuul' in data,
  1708. "%s got %s" % (authz['sub'], data))
  1709. self.assertTrue('admin' in data['zuul'],
  1710. "%s got %s" % (authz['sub'], data))
  1711. self.assertEqual(test_user['zuul.admin'],
  1712. data['zuul']['admin'],
  1713. "%s got %s" % (authz['sub'], data))
  1714. def test_authorizations_no_header(self):
  1715. """Test that missing Authorization header results in HTTP 401"""
  1716. req = self.get_url('/api/user/authorizations')
  1717. self.assertEqual(401, req.status_code, req.text)
  1718. class TestTenantScopedWebApiTokenWithExpiry(BaseTestWeb):
  1719. config_file = 'zuul-admin-web-token-expiry.conf'
  1720. def test_iat_claim_mandatory(self):
  1721. """Test that the 'iat' claim is mandatory when
  1722. max_validity_time is set"""
  1723. authz = {'iss': 'zuul_operator',
  1724. 'sub': 'testuser',
  1725. 'aud': 'zuul.example.com',
  1726. 'zuul': {
  1727. 'admin': ['tenant-one', ]
  1728. },
  1729. 'exp': time.time() + 3600}
  1730. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1731. algorithm='HS256').decode('utf-8')
  1732. resp = self.post_url(
  1733. "api/tenant/tenant-one/project/org/project/autohold",
  1734. headers={'Authorization': 'Bearer %s' % token},
  1735. json={'job': 'project-test1',
  1736. 'count': 1,
  1737. 'reason': 'because',
  1738. 'node_hold_expiration': 36000})
  1739. self.assertEqual(401, resp.status_code)
  1740. resp = self.post_url(
  1741. "api/tenant/tenant-one/project/org/project/enqueue",
  1742. headers={'Authorization': 'Bearer %s' % token},
  1743. json={'trigger': 'gerrit',
  1744. 'change': '2,1',
  1745. 'pipeline': 'check'})
  1746. self.assertEqual(401, resp.status_code)
  1747. resp = self.post_url(
  1748. "api/tenant/tenant-one/project/org/project/enqueue",
  1749. headers={'Authorization': 'Bearer %s' % token},
  1750. json={'trigger': 'gerrit',
  1751. 'ref': 'abcd',
  1752. 'newrev': 'aaaa',
  1753. 'oldrev': 'bbbb',
  1754. 'pipeline': 'check'})
  1755. self.assertEqual(401, resp.status_code)
  1756. def test_token_from_the_future(self):
  1757. authz = {'iss': 'zuul_operator',
  1758. 'sub': 'testuser',
  1759. 'aud': 'zuul.example.com',
  1760. 'zuul': {
  1761. 'admin': ['tenant-one', ],
  1762. },
  1763. 'exp': time.time() + 7200,
  1764. 'iat': time.time() + 3600}
  1765. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1766. algorithm='HS256').decode('utf-8')
  1767. resp = self.post_url(
  1768. "api/tenant/tenant-one/project/org/project/autohold",
  1769. headers={'Authorization': 'Bearer %s' % token},
  1770. json={'job': 'project-test1',
  1771. 'count': 1,
  1772. 'reason': 'because',
  1773. 'node_hold_expiration': 36000})
  1774. self.assertEqual(401, resp.status_code)
  1775. resp = self.post_url(
  1776. "api/tenant/tenant-one/project/org/project/enqueue",
  1777. headers={'Authorization': 'Bearer %s' % token},
  1778. json={'trigger': 'gerrit',
  1779. 'change': '2,1',
  1780. 'pipeline': 'check'})
  1781. self.assertEqual(401, resp.status_code)
  1782. resp = self.post_url(
  1783. "api/tenant/tenant-one/project/org/project/enqueue",
  1784. headers={'Authorization': 'Bearer %s' % token},
  1785. json={'trigger': 'gerrit',
  1786. 'ref': 'abcd',
  1787. 'newrev': 'aaaa',
  1788. 'oldrev': 'bbbb',
  1789. 'pipeline': 'check'})
  1790. self.assertEqual(401, resp.status_code)
  1791. def test_token_expired(self):
  1792. authz = {'iss': 'zuul_operator',
  1793. 'sub': 'testuser',
  1794. 'aud': 'zuul.example.com',
  1795. 'zuul': {
  1796. 'admin': ['tenant-one', ],
  1797. },
  1798. 'exp': time.time() + 3600,
  1799. 'iat': time.time()}
  1800. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1801. algorithm='HS256').decode('utf-8')
  1802. time.sleep(10)
  1803. resp = self.post_url(
  1804. "api/tenant/tenant-one/project/org/project/autohold",
  1805. headers={'Authorization': 'Bearer %s' % token},
  1806. json={'job': 'project-test1',
  1807. 'count': 1,
  1808. 'reason': 'because',
  1809. 'node_hold_expiration': 36000})
  1810. self.assertEqual(401, resp.status_code)
  1811. resp = self.post_url(
  1812. "api/tenant/tenant-one/project/org/project/enqueue",
  1813. headers={'Authorization': 'Bearer %s' % token},
  1814. json={'trigger': 'gerrit',
  1815. 'change': '2,1',
  1816. 'pipeline': 'check'})
  1817. self.assertEqual(401, resp.status_code)
  1818. resp = self.post_url(
  1819. "api/tenant/tenant-one/project/org/project/enqueue",
  1820. headers={'Authorization': 'Bearer %s' % token},
  1821. json={'trigger': 'gerrit',
  1822. 'ref': 'abcd',
  1823. 'newrev': 'aaaa',
  1824. 'oldrev': 'bbbb',
  1825. 'pipeline': 'check'})
  1826. self.assertEqual(401, resp.status_code)
  1827. def test_autohold(self):
  1828. """Test that autohold can be set through the admin web interface"""
  1829. args = {"reason": "some reason",
  1830. "count": 1,
  1831. 'job': 'project-test2',
  1832. 'change': None,
  1833. 'ref': None,
  1834. 'node_hold_expiration': None}
  1835. authz = {'iss': 'zuul_operator',
  1836. 'aud': 'zuul.example.com',
  1837. 'sub': 'testuser',
  1838. 'zuul': {
  1839. 'admin': ['tenant-one', ],
  1840. },
  1841. 'exp': time.time() + 3600,
  1842. 'iat': time.time()}
  1843. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1844. algorithm='HS256').decode('utf-8')
  1845. req = self.post_url(
  1846. 'api/tenant/tenant-one/project/org/project/autohold',
  1847. headers={'Authorization': 'Bearer %s' % token},
  1848. json=args)
  1849. self.assertEqual(200, req.status_code, req.text)
  1850. data = req.json()
  1851. self.assertEqual(True, data)
  1852. # Check result in rpc client
  1853. client = zuul.rpcclient.RPCClient('127.0.0.1',
  1854. self.gearman_server.port)
  1855. self.addCleanup(client.shutdown)
  1856. autohold_requests = client.autohold_list()
  1857. self.assertNotEqual([], autohold_requests)
  1858. self.assertEqual(1, len(autohold_requests))
  1859. ah_request = autohold_requests[0]
  1860. self.assertEqual('tenant-one', ah_request['tenant'])
  1861. self.assertIn('org/project', ah_request['project'])
  1862. self.assertEqual('project-test2', ah_request['job'])
  1863. self.assertEqual(".*", ah_request['ref_filter'])
  1864. self.assertEqual("some reason", ah_request['reason'])
  1865. class TestWebMulti(BaseTestWeb):
  1866. config_file = 'zuul-gerrit-github.conf'
  1867. def test_web_connections_list_multi(self):
  1868. data = self.get_url('api/connections').json()
  1869. gerrit_connection = {
  1870. 'driver': 'gerrit',
  1871. 'name': 'gerrit',
  1872. 'baseurl': 'https://review.example.com',
  1873. 'canonical_hostname': 'review.example.com',
  1874. 'server': 'review.example.com',
  1875. 'port': 29418,
  1876. }
  1877. github_connection = {
  1878. 'baseurl': 'https://api.github.com',
  1879. 'canonical_hostname': 'github.com',
  1880. 'driver': 'github',
  1881. 'name': 'github',
  1882. 'server': 'github.com',
  1883. }
  1884. self.assertEqual([gerrit_connection, github_connection], data)
  1885. class TestCLIViaWebApi(BaseTestWeb):
  1886. config_file = 'zuul-admin-web.conf'
  1887. def test_autohold(self):
  1888. """Test that autohold can be set with the CLI through REST"""
  1889. authz = {'iss': 'zuul_operator',
  1890. 'aud': 'zuul.example.com',
  1891. 'sub': 'testuser',
  1892. 'zuul': {
  1893. 'admin': ['tenant-one', ]
  1894. },
  1895. 'exp': time.time() + 3600}
  1896. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1897. algorithm='HS256').decode('utf-8')
  1898. p = subprocess.Popen(
  1899. [os.path.join(sys.prefix, 'bin/zuul'),
  1900. '--zuul-url', self.base_url, '--auth-token', token,
  1901. 'autohold', '--reason', 'some reason',
  1902. '--tenant', 'tenant-one', '--project', 'org/project',
  1903. '--job', 'project-test2', '--count', '1'],
  1904. stdout=subprocess.PIPE)
  1905. output = p.communicate()
  1906. self.assertEqual(p.returncode, 0, output[0])
  1907. # Check result in rpc client
  1908. client = zuul.rpcclient.RPCClient('127.0.0.1',
  1909. self.gearman_server.port)
  1910. self.addCleanup(client.shutdown)
  1911. autohold_requests = client.autohold_list()
  1912. self.assertNotEqual([], autohold_requests)
  1913. self.assertEqual(1, len(autohold_requests))
  1914. request = autohold_requests[0]
  1915. self.assertEqual('tenant-one', request['tenant'])
  1916. self.assertIn('org/project', request['project'])
  1917. self.assertEqual('project-test2', request['job'])
  1918. self.assertEqual(".*", request['ref_filter'])
  1919. self.assertEqual("some reason", request['reason'])
  1920. self.assertEqual(1, request['max_count'])
  1921. def test_enqueue(self):
  1922. """Test that the CLI can enqueue a change via REST"""
  1923. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  1924. A.addApproval('Code-Review', 2)
  1925. A.addApproval('Approved', 1)
  1926. authz = {'iss': 'zuul_operator',
  1927. 'aud': 'zuul.example.com',
  1928. 'sub': 'testuser',
  1929. 'zuul': {
  1930. 'admin': ['tenant-one', ]
  1931. },
  1932. 'exp': time.time() + 3600}
  1933. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1934. algorithm='HS256').decode('utf-8')
  1935. p = subprocess.Popen(
  1936. [os.path.join(sys.prefix, 'bin/zuul'),
  1937. '--zuul-url', self.base_url, '--auth-token', token,
  1938. 'enqueue', '--tenant', 'tenant-one',
  1939. '--trigger', 'gerrit', '--project', 'org/project',
  1940. '--pipeline', 'gate', '--change', '1,1'],
  1941. stdout=subprocess.PIPE)
  1942. output = p.communicate()
  1943. self.assertEqual(p.returncode, 0, output[0])
  1944. self.waitUntilSettled()
  1945. def test_enqueue_ref(self):
  1946. """Test that the CLI can enqueue a ref via REST"""
  1947. p = "review.example.com/org/project"
  1948. upstream = self.getUpstreamRepos([p])
  1949. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  1950. A.setMerged()
  1951. A_commit = str(upstream[p].commit('master'))
  1952. self.log.debug("A commit: %s" % A_commit)
  1953. authz = {'iss': 'zuul_operator',
  1954. 'aud': 'zuul.example.com',
  1955. 'sub': 'testuser',
  1956. 'zuul': {
  1957. 'admin': ['tenant-one', ]
  1958. },
  1959. 'exp': time.time() + 3600}
  1960. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1961. algorithm='HS256').decode('utf-8')
  1962. p = subprocess.Popen(
  1963. [os.path.join(sys.prefix, 'bin/zuul'),
  1964. '--zuul-url', self.base_url, '--auth-token', token,
  1965. 'enqueue-ref', '--tenant', 'tenant-one',
  1966. '--trigger', 'gerrit', '--project', 'org/project',
  1967. '--pipeline', 'post', '--ref', 'master',
  1968. '--oldrev', '90f173846e3af9154517b88543ffbd1691f31366',
  1969. '--newrev', A_commit],
  1970. stdout=subprocess.PIPE)
  1971. output = p.communicate()
  1972. self.assertEqual(p.returncode, 0, output[0])
  1973. self.waitUntilSettled()
  1974. def test_dequeue(self):
  1975. """Test that the CLI can dequeue a change via REST"""
  1976. start_builds = len(self.builds)
  1977. self.create_branch('org/project', 'stable')
  1978. self.executor_server.hold_jobs_in_build = True
  1979. self.commitConfigUpdate('common-config', 'layouts/timer.yaml')
  1980. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  1981. self.waitUntilSettled()
  1982. for _ in iterate_timeout(30, 'Wait for a build on hold'):
  1983. if len(self.builds) > start_builds:
  1984. break
  1985. self.waitUntilSettled()
  1986. authz = {'iss': 'zuul_operator',
  1987. 'aud': 'zuul.example.com',
  1988. 'sub': 'testuser',
  1989. 'zuul': {
  1990. 'admin': ['tenant-one', ]
  1991. },
  1992. 'exp': time.time() + 3600}
  1993. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1994. algorithm='HS256').decode('utf-8')
  1995. p = subprocess.Popen(
  1996. [os.path.join(sys.prefix, 'bin/zuul'),
  1997. '--zuul-url', self.base_url, '--auth-token', token,
  1998. 'dequeue', '--tenant', 'tenant-one', '--project', 'org/project',
  1999. '--pipeline', 'periodic', '--ref', 'refs/heads/stable'],
  2000. stdout=subprocess.PIPE)
  2001. output = p.communicate()
  2002. self.assertEqual(p.returncode, 0, output[0])
  2003. self.waitUntilSettled()
  2004. self.commitConfigUpdate('common-config',
  2005. 'layouts/no-timer.yaml')
  2006. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  2007. self.waitUntilSettled()
  2008. self.executor_server.hold_jobs_in_build = False
  2009. self.executor_server.release()
  2010. self.waitUntilSettled()
  2011. self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 1)