This commit is contained in:
Christophe Monniez 2022-07-13 16:49:21 +02:00
parent e1d74d8582
commit c691323999
11 changed files with 416 additions and 2 deletions

View File

@ -3,3 +3,4 @@
from . import frontend
from . import hook
from . import badge
from . import jsonroutes

View File

@ -0,0 +1,118 @@
import json
from functools import wraps
from odoo.exceptions import AccessError
from odoo.http import Controller, request, route
from odoo.osv import expression
RECORDS_PER_PAGE = 100
def to_json(fn):
@wraps(fn)
def decorator(*args, **kwargs):
headers = [('Content-Type', 'application/json'),
('Cache-Control', 'no-store')]
try:
return request.make_response(json.dumps(fn(*args, **kwargs)._get_description(), indent=4, default=str), headers)
except AccessError:
response = request.make_response(json.dumps('unauthorized'), headers)
response.status = 403
return response
return decorator
class RunbotJsonRoutes(Controller):
@route(['/runbot/json/projects',
'/runbot/json/projects/<int:project_id>'], type='http', auth='public')
@to_json
def projects(self, project_id=None, **kwargs):
if project_id:
projects = request.env['runbot.project'].browse(project_id)
else:
domain = ([('group_ids', '=', False)]) if request.env.user._is_public() else []
projects = request.env['runbot.project'].search(domain)
return projects
@route(['/runbot/json/bundles/<int:bundle_id>',
'/runbot/json/projects/<int:project_id>/bundles'], type='http', auth='public')
@to_json
def bundles(self, project_id=None, bundle_id=None, page=0, **kwargs):
offset = int(page) * RECORDS_PER_PAGE
domain = []
if bundle_id:
bundles = request.env['runbot.bundle'].browse(bundle_id)
else:
domain = [('project_id', '=', project_id)]
if 'sticky' in kwargs:
domain = expression.AND([domain, [('sticky', '=', kwargs['sticky'])]])
if 'name' in kwargs:
name_query = kwargs['name']
name_query = name_query if len(name_query) < 60 else name_query[:60]
domain = expression.AND([domain, [('name', 'ilike', name_query)]])
bundles = request.env['runbot.bundle'].search(domain, order='id desc', limit=RECORDS_PER_PAGE, offset=offset)
return bundles
@route(['/runbot/json/bundles/<int:bundle_id>/batches',
'/runbot/json/batches/<int:batch_id>'], type='http', auth='public')
@to_json
def batches(self, bundle_id=None, batch_id=None, page=0, **kwargs):
offset = int(page) * RECORDS_PER_PAGE
if batch_id:
batches = request.env['runbot.batch'].browse(batch_id)
else:
domain = [('bundle_id', '=', bundle_id)] if bundle_id else []
domain += [('state', '=', kwargs['state'])] if 'state' in kwargs else []
batches = request.env['runbot.batch'].search(domain, order="id desc", limit=RECORDS_PER_PAGE, offset=offset)
return batches
@route(['/runbot/json/batches/<int:batch_id>/commits',
'/runbot/json/commits/<int:commit_id>'], type='http', auth='public')
@to_json
def commits(self, commit_id=None, batch_id=None, page=0, **kwargs):
if commit_id:
commits = request.env['runbot.commit'].browse(commit_id)
else:
commits = request.env['runbot.batch'].browse(batch_id).commit_ids
return commits
@route(['/runbot/json/commits/<int:commit_id>/commit_links',
'/runbot/json/commit_links/<int:commit_link_id>'], type='http', auth='public')
@to_json
def commit_links(self, commit_id=None, commit_link_id=None, page=0, **kwargs):
if commit_link_id:
commit_links = request.env['runbot.commit.link'].browse(commit_link_id)
else:
domain = [('commit_id', '=', commit_id)]
commit_links = request.env['runbot.commit.link'].search(domain)
return commit_links
@route(['/runbot/json/repos',
'/runbot/json/repos/<int:repo_id>'], type='http', auth='public')
@to_json
def repos(self, repo_id=None, page=0, **kwargs):
if repo_id:
repos = request.env['runbot.repo'].browse(repo_id)
else:
domain = []
repos = request.env['runbot.repo'].search(domain)
return repos
@route(['/runbot/json/batches/<int:batch_id>/slots',
'/runbot/json/batch_slots/<int:slot_id>'], type='http', auth='public')
@to_json
def batch_slots(self, batch_id=None, slot_id=None, page=0, **kwargs):
if slot_id:
slots = request.env['runbot.batch.slot'].browse(slot_id)
else:
slots = request.env['runbot.batch'].browse(batch_id).slot_ids
return slots
@route(['/runbot/json/batches/<int:batch_id>/builds',
'/runbot/json/builds/<int:build_id>'], type='http', auth='public')
@to_json
def builds(self, build_id=None, batch_id=None, page=0, **kwargs):
if build_id:
builds = request.env['runbot.build'].browse(build_id)
else:
builds = request.env['runbot.batch'].browse(batch_id).all_build_ids
return builds

View File

@ -176,7 +176,6 @@ class Batch(models.Model):
lambda t: not t.version_domain or \
self.bundle_id.version_id.filtered_domain(t.get_version_domain())
)
pushed_repo = self.commit_link_ids.mapped('commit_id.repo_id')
dependency_repos = triggers.mapped('dependency_ids')
all_repos = triggers.mapped('repo_ids') | dependency_repos
@ -327,7 +326,6 @@ class Batch(models.Model):
'used_custom_trigger': bool(trigger_custom),
}
params_value['builds_reference_ids'] = trigger._reference_builds(bundle)
params = self.env['runbot.build.params'].create(params_value)
build = self.env['runbot.build']
@ -412,6 +410,23 @@ class Batch(models.Model):
'level': level,
})
def _get_description(self):
return[
{
'id': r.id,
'url': f'{r.get_base_url()}/runbot/json/batches/{r.id}',
'state': r.state,
'last_update': r.last_update,
'bundle_name' : r.bundle_id.name,
'bundle_url': f'{r.get_base_url()}/runbot/json/bundles/{r.bundle_id.id}',
'commits_url': f'{r.get_base_url()}/runbot/json/batches/{r.id}/commits',
'commits': [info for commit in r.commit_ids for info in commit._get_description()],
'slots_url': f'{r.get_base_url()}/runbot/json/batches/{r.id}/slots',
'slots' : [info for slot in r.slot_ids for info in slot._get_description()],
}
for r in self
]
class BatchLog(models.Model):
_name = 'runbot.batch.log'
@ -467,3 +482,19 @@ class BatchSlot(models.Model):
self.batch_id._log(f'Trigger {self.trigger_id.name} was started by {self.env.user.name}')
self.link_type, self.build_id = self.batch_id._create_build(self.params_id)
return self.build_id
def _get_description(self):
return[
{
'id': r.id,
'url': f'{r.get_base_url()}/runbot/json/batch_slots/{r.id}',
'trigger_name': r.trigger_id.name,
'link_type': r.link_type,
'batch_url': f'{r.get_base_url()}/runbot/json/batches/{r.batch_id.id}',
'build_url': f'{r.get_base_url()}/runbot/json/builds/{r.build_id.id}' if r.build_id else False,
'active': r.active,
'skipped': r.skipped,
'build': r.build_id._get_description()[0] if r.build_id else False,
}
for r in self
]

View File

@ -1202,3 +1202,25 @@ class BuildResult(models.Model):
def parse_config(self):
return set(findall(self._server("tools/config.py"), '--[\w-]+', ))
def _get_description(self):
return[
{
'id': r.id,
'dest': r.dest,
'url': f'{r.get_base_url()}/runbot/json/builds/{r.id}',
'parent_build_url': f'{r.get_base_url()}/runbot/json/builds/{r.parent_id}' if r.parent_id else False,
'params_url': f'{r.get_base_url()}/runbot/json/build_params/{r.params_id.id}',
'version': r.version_id.name,
'config': r.config_id.name,
'trigger': r.trigger_id.name,
'global_state': r.global_state,
'local_state': r.local_state,
'global_result': r.global_result,
'local_result': r.local_result,
'triggered_result': r.triggered_result,
'host': r.host,
'children': [info for child in r.children_ids for info in child._get_description()],
}
for r in self
]

View File

@ -246,3 +246,19 @@ class Bundle(models.Model):
for branch in self.branch_ids.sorted(key=lambda b: (b.is_pr)):
branch_groups[branch.remote_id.repo_id].append(branch)
return branch_groups
def _get_description(self):
return[
{
'id': r.id,
'url': f'{r.get_base_url()}/runbot/json/bundles/{r.id}',
'name': r.name,
'sticky': r.sticky,
'version_number': r.version_number,
'project_url': f'{r.get_base_url()}/runbot/json/projects/{r.project_id.id}',
'last_batch_url': f'{r.get_base_url()}/runbot/json/batches/{r.last_batch.id}',
'last_batch' : r.last_batch._get_description()[0] if r.last_batch else False,
'batches_url': f'{r.get_base_url()}/runbot/json/bundles/{r.id}/batches'
}
for r in self
]

View File

@ -153,6 +153,23 @@ class Commit(models.Model):
'to_process': True,
})
def _get_description(self):
return[
{
'id': r.id,
'url': f'{r.get_base_url()}/runbot/json/commits/{r.id}',
'hash': r.name,
'repo': r.repo_id.name,
'repo_url': f'{r.get_base_url()}/runbot/json/repos/{r.repo_id.id}',
'author': r.author,
'author_email': r.author_email,
'committer': r.committer,
'committer_email': r.committer_email,
'subject': r.subject,
'commit_links_url': f'{r.get_base_url()}/runbot/json/commits/{r.id}/commit_links',
}
for r in self
]
class CommitLink(models.Model):
_name = 'runbot.commit.link'
@ -171,6 +188,30 @@ class CommitLink(models.Model):
diff_add = fields.Integer('# line added')
diff_remove = fields.Integer('# line removed')
def _get_description(self):
return[
{
'id': r.id,
'url': f'{r.get_base_url()}/runbot/json/commit_links/{r.id}',
'commit_url': f'{r.get_base_url()}/runbot/json/commits/{r.id}',
'hash': r.commit_id.name,
'match_type': r.match_type,
'author': r.commit_id.author,
'author_email': r.commit_id.author_email,
'committer': r.commit_id.committer,
'committer_email': r.commit_id.committer_email,
'subject': r.commit_id.subject,
'base_commit': r.base_commit_id.name,
'base_commit_url': f'{r.get_base_url()}/runbot/json/commits/{r.base_commit_id.id}' if r.base_commit_id else False,
'merge_base_commit': r.merge_base_commit_id.name,
'merge_base_commit_url': f'{r.get_base_url()}/runbot/json/commits/{r.merge_base_commit_id.id}' if r.merge_base_commit_id else False,
'branch_url': f'{r.get_base_url()}/runbot/json/branches/{r.id}',
'branch_name': r.branch_id.name,
'github_branch_url': f'https://{r.branch_id.remote_id.base_url}/commit/{r.commit_id.name}',
}
for r in self
]
class CommitStatus(models.Model):
_name = 'runbot.commit.status'

View File

@ -42,6 +42,17 @@ class Project(models.Model):
project.dummy_bundle_id = bundle
return projects
def _get_description(self):
return[
{
'id': r.id,
'url': f'{r.get_base_url()}/runbot/json/projects/{r.id}',
'name': r.name,
'keep_sticky_running': r.keep_sticky_running,
'bundles_url': f'{r.get_base_url()}/runbot/json/projects/{r.id}/bundles'
}
for r in self
]
class Category(models.Model):
_name = 'runbot.category'

View File

@ -563,6 +563,22 @@ class Repo(models.Model):
if file.startswith(base_path):
return file[len(base_path):].strip('/').split('/')[0]
def _get_description(self):
return[
{
'id': r.id,
'url': f'{r.get_base_url()}/runbot/json/repos/{r.id}',
'name': r.name,
'project': r.project_id.name,
'project_url': f'{r.get_base_url()}/runbot/json/projects/{r.project_id.id}',
'hook_time': r.hook_time,
'hook_time_string': datetime.datetime.fromtimestamp(r.hook_time),
'last_processed_hook_time': r.last_processed_hook_time,
'last_processed_hook_time_string': datetime.datetime.fromtimestamp(r.last_processed_hook_time),
}
for r in self
]
class RefTime(models.Model):
_name = 'runbot.repo.reftime'

View File

@ -130,5 +130,35 @@
<field name="perm_read" eval="False"/>
</record>
<record id="runbot_bundle_access_user" model="ir.rule">
<field name="name">User can read bundle from public projects or specific groups</field>
<field name="model_id" ref="model_runbot_bundle"/>
<field name="domain_force">['|', ('project_id.group_ids.users', 'in', user.id), ('project_id.group_ids', '=', False)]</field>
</record>
<record id="runbot_batch_access_user" model="ir.rule">
<field name="name">User can read batch from public projects or specific groups</field>
<field name="model_id" ref="model_runbot_batch"/>
<field name="domain_force">['|', ('bundle_id.project_id.group_ids.users', 'in', user.id), ('bundle_id.project_id.group_ids', '=', False)]</field>
</record>
<record id="runbot_commit_access_user" model="ir.rule">
<field name="name">User can read commits from public projects or specific groups</field>
<field name="model_id" ref="model_runbot_commit"/>
<field name="domain_force">['|', ('repo_id.project_id.group_ids.users', 'in', user.id), ('repo_id.project_id.group_ids', '=', False)]</field>
</record>
<record id="runbot_batch_slot_access_user" model="ir.rule">
<field name="name">User can read batch slot from public projects or specific groups</field>
<field name="model_id" ref="model_runbot_batch_slot"/>
<field name="domain_force">['|', ('batch_id.bundle_id.project_id.group_ids.users', 'in', user.id), ('batch_id.bundle_id.project_id.group_ids', '=', False)]</field>
</record>
<record id="runbot_build_access_user" model="ir.rule">
<field name="name">User can read build from public projects or specific groups</field>
<field name="model_id" ref="model_runbot_build"/>
<field name="domain_force">['|', ('params_id.project_id.group_ids.users', 'in', user.id), ('params_id.project_id.group_ids', '=', False)]</field>
</record>
</data>
</odoo>

View File

@ -15,3 +15,4 @@ from . import test_commit
from . import test_upgrade
from . import test_dockerfile
from . import test_host
from . import test_json_routes

View File

@ -0,0 +1,127 @@
import json
from odoo import fields
from odoo.tests.common import HttpCase
from .common import RunbotCase
class TestJsonRoutes(RunbotCase, HttpCase):
def setUp(self):
super().setUp()
self.additionnal_setup()
self.private_group = self.env['res.groups'].create({
'name': 'Test Private',
})
self.private_project = self.Project.create({
'name': 'Private Test Project',
'group_ids': [fields.Command.link(self.private_group.id)],
})
self.private_repo_server = self.Repo.create({
'name': 'private_server',
'project_id': self.private_project.id,
'server_files': 'server.py',
'addons_paths': 'addons,core/addons'
})
self.private_remote_server = self.Remote.create({
'name': 'foo@example.com:base/server',
'repo_id': self.private_repo_server.id,
'token': '456',
})
self.initial_private_server_commit = self.Commit.create({
'name': 'afafafaf',
'repo_id': self.private_repo_server.id,
'date': '2020-01-01',
'subject': 'Initial Private Commit',
'author': 'foofoo',
'author_email': 'foofoo@somewhere.com'
})
self.private_branch_server = self.Branch.create({
'name': '13.0',
'remote_id': self.private_remote_server.id,
'is_pr': False,
'head': self.initial_private_server_commit.id,
})
self.branch_server.bundle_id.is_base = True
self.Trigger.create({
'name': 'Private Repo Server trigger',
'repo_ids': [(4, self.private_repo_server.id)],
'config_id': self.default_config.id,
'project_id': self.private_project.id,
})
def check_json_route(self, route, expected_status):
response = self.url_open(route)
self.assertEqual(response.status_code, expected_status)
res = json.loads(response.content)
if expected_status == 403:
self.assertEqual(res, 'unauthorized')
return res
def test_json_flow_public_user(self):
# test that a public user can get public project informations
projects_infos = self.check_json_route('/runbot/json/projects', 200)
project_names = [p['name'] for p in projects_infos]
self.assertIn(self.project.name, project_names)
# test that a public user cannot get a private project informations
self.assertNotIn('Private Project', project_names)
self.check_json_route(f'/runbot/json/projects/{self.private_project.id}', 403)
bundles_infos = self.check_json_route(projects_infos[0]['bundles_url'], 200)
bundles_names = [b['name'] for b in bundles_infos]
self.assertIn(self.branch_server.bundle_id.name, bundles_names)
# check that public user cannot access private project bundles
private_bundle = self.Bundle.search([('name', '=', '13.0'), ('project_id', '=', self.private_project.id)])
self.check_json_route(f'/runbot/json/bundles/{private_bundle.id}', 403)
private_batch = self.private_branch_server.bundle_id._force()
private_batch._prepare()
batches_infos = self.check_json_route(f'/runbot/json/bundles/{self.branch_server.bundle_id.id}/batches', 200)
batch_ids = [ b['id'] for b in batches_infos]
self.assertEqual(len(batch_ids), 1)
self.assertNotIn(private_batch.id, batch_ids)
# Let's verify that the batches infos contains the commits informations too
for commit in batches_infos[0]['commits']:
self.assertIn(commit['hash'], ['aaaaaaa', 'cccccc'])
# check that a public user cannot access private project batches
private_batches_infos = self.check_json_route(f'/runbot/json/bundles/{self.private_branch_server.bundle_id.id}/batches', 200)
self.assertEqual(private_batches_infos, [])
commits_infos = self.check_json_route(batches_infos[0]['commits_url'], 200)
self.check_json_route(commits_infos[0]['url'], 200)
# check that a public user cannot access private project commits
self.check_json_route(f'/runbot/json/batches/{private_batch.id}/commits', 403)
self.check_json_route(f'/runbot/json/commits/{self.initial_private_server_commit.id}', 403)
commit_links_infos = self.check_json_route(commits_infos[0]['commit_links_url'], 200)
self.check_json_route(commit_links_infos[0]['url'], 200)
# check that a public user cannot access private project commit links
private_commit_link = self.env['runbot.commit.link'].search([('commit_id', '=', self.initial_private_server_commit.id)], limit=1)
self.check_json_route(f'/runbot/json/commit_links/{private_commit_link.id}', 403)
server_slot_infos = list(filter(lambda slot: slot['trigger_name'] == 'Server trigger', self.check_json_route(batches_infos[0]['slots_url'], 200)))
# check that a public user cannot access private project batch slots
self.check_json_route(f'/runbot/json/batches/{private_batch.id}/slots', 403)
private_slot = self.env['runbot.batch.slot'].search([('batch_id', '=', private_batch.id)], limit=1)
self.check_json_route(f'/runbot/json/batch_slots/{private_slot.id}', 403)
server_build_infos = self.check_json_route(server_slot_infos[0]['build_url'], 200)
self.assertEqual(server_build_infos[0]['trigger'], 'Server trigger')
# check that a public user cannot access private project build
self.check_json_route(f'/runbot/json/batches/{private_batch.id}/builds', 403)
private_build = self.env['runbot.build'].search([('params_id.project_id','=', self.private_project.id)])
self.check_json_route(f'/runbot/json/builds/{private_build.id}', 403)