Maciej Wawro
6 years ago
19 changed files with 530 additions and 0 deletions
-
1galicea_git/README.md
-
5galicea_git/__init__.py
-
48galicea_git/__manifest__.py
-
3galicea_git/controllers/__init__.py
-
83galicea_git/controllers/main.py
-
9galicea_git/data/config.xml
-
63galicea_git/http_chunked_fix.py
-
4galicea_git/models/__init__.py
-
42galicea_git/models/config_settings.py
-
100galicea_git/models/repository.py
-
3galicea_git/security/ir.model.access.csv
-
47galicea_git/security/security.xml
-
BINgalicea_git/static/description/icon.png
-
BINgalicea_git/static/description/images/config_screenshot.png
-
BINgalicea_git/static/description/images/console_screenshot.png
-
BINgalicea_git/static/description/images/create_screenshot.png
-
19galicea_git/static/description/index.html
-
37galicea_git/system_checks.py
-
66galicea_git/views/views.xml
@ -0,0 +1 @@ |
|||
[See add-on page on odoo.com](https://apps.odoo.com/apps/modules/10.0/galicea_git/) |
@ -0,0 +1,5 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
from . import controllers |
|||
from . import models |
|||
from . import system_checks |
@ -0,0 +1,48 @@ |
|||
# -*- coding: utf-8 -*- |
|||
{ |
|||
'name': "Galicea Git hosting", |
|||
|
|||
'summary': """Git repository hosting and per-user access checking""", |
|||
|
|||
'author': "Maciej Wawro", |
|||
'maintainer': "Galicea", |
|||
'website': "http://galicea.pl", |
|||
|
|||
'category': 'Technical Settings', |
|||
'version': '10.0.1.0', |
|||
|
|||
'depends': ['web', 'galicea_environment_checkup'], |
|||
|
|||
'external_dependencies': { |
|||
'bin': ['git'] |
|||
}, |
|||
|
|||
'data': [ |
|||
'security/security.xml', |
|||
'security/ir.model.access.csv', |
|||
|
|||
'data/config.xml', |
|||
'views/views.xml', |
|||
], |
|||
|
|||
'images': [ |
|||
'static/description/images/create_screenshot.png', |
|||
'static/description/images/config_screenshot.png', |
|||
'static/description/images/console_screenshot.png', |
|||
], |
|||
|
|||
'application': True, |
|||
'installable': True, |
|||
|
|||
'environment_checkup': { |
|||
'dependencies': { |
|||
'external': [ |
|||
{ |
|||
'name': 'git', |
|||
'version': '>=2.1.4', |
|||
'install': "apt install git" |
|||
} |
|||
] |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,3 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
from . import main |
@ -0,0 +1,83 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import subprocess, os, io |
|||
|
|||
from odoo import http |
|||
from odoo.tools import config |
|||
import werkzeug |
|||
|
|||
from ..http_chunked_fix import http_input_stream |
|||
|
|||
class Main(http.Controller): |
|||
@http.route( |
|||
[ |
|||
'/git/<repo>', |
|||
'/git/<repo>/<path:path>', |
|||
], |
|||
auth='public', |
|||
csrf=False |
|||
) |
|||
def git(self, request, repo, **kw): |
|||
auth = request.httprequest.authorization |
|||
if auth: |
|||
request.session.authenticate(request.session.db, auth.username, auth.password) |
|||
if not request.env.uid or request.env.user.login == 'public': |
|||
return werkzeug.Response( |
|||
headers=[('WWW-Authenticate', 'Basic')], |
|||
status=401 |
|||
) |
|||
|
|||
try: |
|||
repository = request.env['galicea_git.repository'].search( |
|||
[('system_name', '=', repo)] |
|||
) |
|||
except AccessError: |
|||
return werkzeug.Response( |
|||
status=403 |
|||
) |
|||
if not repository.exists(): |
|||
return werkzeug.Response( |
|||
status=404 |
|||
) |
|||
|
|||
http_environment = request.httprequest.environ |
|||
git_env = { |
|||
'REQUEST_METHOD': http_environment['REQUEST_METHOD'], |
|||
'QUERY_STRING': http_environment['QUERY_STRING'], |
|||
'CONTENT_TYPE': request.httprequest.headers.get('Content-Type'), |
|||
'REMOTE_ADDR': http_environment['REMOTE_ADDR'], |
|||
'GIT_PROJECT_ROOT': os.path.join(config['data_dir'], 'git'), |
|||
'GIT_HTTP_EXPORT_ALL': '1', |
|||
'PATH_INFO': http_environment['PATH_INFO'][4:], |
|||
'REMOTE_USER': request.env.user.login |
|||
} |
|||
|
|||
command_env = os.environ.copy() |
|||
for var in git_env: |
|||
command_env[var] = git_env[var] |
|||
|
|||
git = subprocess.Popen( |
|||
['/usr/lib/git-core/git-http-backend'], |
|||
stdin=subprocess.PIPE, |
|||
stdout=subprocess.PIPE, |
|||
stderr=subprocess.PIPE, |
|||
env=command_env, |
|||
shell=True |
|||
) |
|||
stdout, stderr = git.communicate(http_input_stream(request).read()) |
|||
headers_str, body = stdout.split("\r\n\r\n", 2) |
|||
|
|||
http_status_code = 200 |
|||
headers = [] |
|||
for header in headers_str.split("\r\n"): |
|||
name, value = header.split(': ', 2) |
|||
if name == 'Status': |
|||
http_code = int(value.split(' ')[0]) |
|||
else: |
|||
headers.append((name, value)) |
|||
|
|||
return werkzeug.Response( |
|||
body, |
|||
status = http_status_code, |
|||
headers = headers |
|||
) |
@ -0,0 +1,9 @@ |
|||
<odoo> |
|||
<data noupdate="1"> |
|||
<record id="config_git_backend_path" model="ir.config_parameter"> |
|||
<field name="key">galicea_git.git_http_backend</field> |
|||
<field name="value">/usr/lib/git-core/git-http-backend</field> |
|||
<field name="group_ids" eval="[(4, ref('galicea_git.group_admin'))]" /> |
|||
</record> |
|||
</data> |
|||
</odoo> |
@ -0,0 +1,63 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import io |
|||
|
|||
# Werkzeug version bundled into odoo doesn't handle this kind of Transfer-Encoding |
|||
# correctly. We copy the fix from https://github.com/pallets/werkzeug/pull/1198/files |
|||
class DechunkedInput(io.RawIOBase): |
|||
"""An input stream that handles Transfer-Encoding 'chunked'""" |
|||
|
|||
def __init__(self, rfile): |
|||
self._rfile = rfile |
|||
self._done = False |
|||
self._len = 0 |
|||
|
|||
def readable(self): |
|||
return True |
|||
|
|||
def read_chunk_len(self): |
|||
try: |
|||
line = self._rfile.readline().decode('latin1') |
|||
_len = int(line.strip(), 16) |
|||
except ValueError: |
|||
raise IOError('Invalid chunk header') |
|||
if _len < 0: |
|||
raise IOError('Negative chunk length not allowed') |
|||
return _len |
|||
|
|||
def readinto(self, buf): |
|||
read = 0 |
|||
while not self._done and read < len(buf): |
|||
if self._len == 0: |
|||
# This is the first chunk or we fully consumed the previous |
|||
# one. Read the next length of the next chunk |
|||
self._len = self.read_chunk_len() |
|||
|
|||
if self._len == 0: |
|||
# Found the final chunk of size 0. The stream is now exhausted, |
|||
# but there is still a final newline that should be consumed |
|||
self._done = True |
|||
|
|||
if self._len > 0: |
|||
# There is data (left) in this chunk, so append it to the |
|||
# buffer. If this operation fully consumes the chunk, this will |
|||
# reset self._len to 0. |
|||
n = min(len(buf), self._len) |
|||
buf[read:read + n] = self._rfile.read(n) |
|||
self._len -= n |
|||
read += n |
|||
|
|||
if self._len == 0: |
|||
# Skip the terminating newline of a chunk that has been fully |
|||
# consumed. This also applies to the 0-sized final chunk |
|||
terminator = self._rfile.readline() |
|||
if terminator not in (b'\n', b'\r\n', b'\r'): |
|||
raise IOError('Missing chunk terminating newline') |
|||
|
|||
return read |
|||
|
|||
def http_input_stream(request): |
|||
if request.httprequest.headers.get('Transfer-Encoding') == 'chunked' \ |
|||
and not request.httprequest.environ.get('wsgi.input_terminated'): |
|||
return DechunkedInput(request.httprequest.environ['wsgi.input']) |
|||
return request.httprequest.stream |
@ -0,0 +1,4 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
from . import repository |
|||
from . import config_settings |
@ -0,0 +1,42 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import os |
|||
|
|||
from odoo import models, fields, api |
|||
from odoo.exceptions import ValidationError |
|||
|
|||
class ConfigSettings(models.TransientModel): |
|||
_name = 'galicea_git.config.settings' |
|||
_inherit = 'res.config.settings' |
|||
|
|||
git_http_backend = fields.Char( |
|||
'Absolute path to Git HTTP backend', |
|||
required=True |
|||
) |
|||
git_http_backend_valid = fields.Boolean( |
|||
compute='_compute_git_http_backend_valid' |
|||
) |
|||
|
|||
@api.one |
|||
@api.depends('git_http_backend') |
|||
def _compute_git_http_backend_valid(self): |
|||
self.git_http_backend_valid = self.git_http_backend and os.access(self.git_http_backend, os.X_OK) |
|||
|
|||
@api.one |
|||
def set_params(self): |
|||
self.env['ir.config_parameter'].set_param('galicea_git.git_http_backend', self.git_http_backend) |
|||
|
|||
@api.model |
|||
def get_default_values(self, fields): |
|||
return { |
|||
'git_http_backend': self.env['ir.config_parameter'].get_param('galicea_git.git_http_backend') |
|||
} |
|||
|
|||
@api.multi |
|||
def execute(self): |
|||
self.ensure_one() |
|||
if not self.env.user.has_group('galicea_git.group_admin'): |
|||
raise AccessError("Only Git administrators can change those settings") |
|||
super(ConfigSettings, self.sudo()).execute() |
|||
act_window = self.env.ref('galicea_git.config_settings_action') |
|||
return act_window.read()[0] |
@ -0,0 +1,100 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import os |
|||
import random |
|||
import shutil |
|||
import string |
|||
import subprocess |
|||
|
|||
from odoo import models, fields, api, http |
|||
from odoo.exceptions import ValidationError |
|||
from odoo.tools import config, which |
|||
|
|||
class Repository(models.Model): |
|||
_name = 'galicea_git.repository' |
|||
|
|||
state = fields.Selection( |
|||
[('draft', 'Draft'), ('created', 'Created')], |
|||
default='draft' |
|||
) |
|||
|
|||
name = fields.Char('User-friendly name', required=True) |
|||
system_name = fields.Char( |
|||
'Directory name', |
|||
required=True, |
|||
readonly=True, |
|||
index=True, |
|||
states={'draft': [('readonly', False)]} |
|||
) |
|||
collaborator_ids = fields.Many2many( |
|||
'res.users', |
|||
string='Collaborators' |
|||
) |
|||
|
|||
local_directory = fields.Char( |
|||
'Local directory on server', |
|||
compute='_compute_local_directory', |
|||
groups='galicea_git.group_admin' |
|||
) |
|||
url = fields.Char( |
|||
'Clone', |
|||
compute='_compute_url' |
|||
) |
|||
|
|||
@api.one |
|||
@api.depends('system_name') |
|||
def _compute_url(self): |
|||
base_url = http.request.httprequest.host_url if http.request \ |
|||
else env['ir.config_parameter'].get_param('web.base.url') + '/' |
|||
self.url = u'{}git/{}'.format(base_url, self.system_name) |
|||
|
|||
@api.one |
|||
@api.depends('system_name') |
|||
def _compute_local_directory(self): |
|||
if self.system_name: |
|||
self.local_directory = os.path.join(config['data_dir'], 'git', self.system_name) |
|||
|
|||
@api.constrains('system_name') |
|||
def _validate_system_name(self): |
|||
allowed_characters = string.ascii_lowercase + string.digits + '-' |
|||
if not all(c in allowed_characters for c in self.system_name): |
|||
raise ValidationError( |
|||
'Only lowercase, digits and hyphens (-) are allowed in directory name' |
|||
) |
|||
|
|||
@api.constrains('collaborator_ids') |
|||
def _validate_collaborator_ids(self): |
|||
invalid_collaborators = self.collaborator_ids.filtered(lambda c: not c.has_group('galicea_git.group_collaborator')) |
|||
if invalid_collaborators: |
|||
raise ValidationError( |
|||
'User {} does not have the {} role. Contact your Administrator'.format( |
|||
invalid_collaborators[0].name, |
|||
self.env.ref('galicea_git.group_collaborator').full_name |
|||
) |
|||
) |
|||
|
|||
@api.model |
|||
def create(self, values): |
|||
values['state'] = 'created' |
|||
ret = super(Repository, self).create(values) |
|||
ret.__initialize_repo() |
|||
return ret |
|||
|
|||
@api.multi |
|||
def unlink(selfs): |
|||
directories_to_move = selfs.mapped(lambda r: r.local_directory) |
|||
ret = super(Repository, selfs).unlink() |
|||
for directory in directories_to_move: |
|||
if os.path.exists(directory): |
|||
suffix = ''.join(random.choice(string.ascii_lowercase) for _ in range(8)) |
|||
new_directory = directory + '-deleted-' + suffix |
|||
shutil.move(directory, new_directory) |
|||
|
|||
@api.multi |
|||
def __initialize_repo(self): |
|||
self.ensure_one() |
|||
if os.path.exists(self.local_directory): |
|||
raise ValidationError( |
|||
'Repository {} already exists, choose a different name!'.format(self.system_name) |
|||
) |
|||
subprocess.check_call([which('git'), 'init', '--bare', self.local_directory]) |
@ -0,0 +1,3 @@ |
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink |
|||
access_repository_collaborator,repository_collaborator,model_galicea_git_repository,galicea_git.group_collaborator,1,0,0,0 |
|||
access_repository_admin,repository_admin,model_galicea_git_repository,galicea_git.group_admin,1,1,1,1 |
@ -0,0 +1,47 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<record id="module_category_git" model="ir.module.category"> |
|||
<field name="name">Git</field> |
|||
</record> |
|||
|
|||
<record id="group_collaborator" model="res.groups"> |
|||
<field name="name">Collaborator</field> |
|||
<field name="category_id" ref="module_category_git" /> |
|||
</record> |
|||
|
|||
<record id="group_admin" model="res.groups"> |
|||
<field name="name">Administrator</field> |
|||
<field name="category_id" ref="module_category_git" /> |
|||
<field name="implied_ids" eval="[(4,ref('group_collaborator'))]" /> |
|||
</record> |
|||
|
|||
<record id="base.group_erp_manager" model="res.groups"> |
|||
<field name="implied_ids" eval="[(4,ref('group_admin'))]" /> |
|||
</record> |
|||
|
|||
<record id="repository_collaborator_access_rule" model="ir.rule"> |
|||
<field name="name">Collaborators can only access repositories they are assigned to</field> |
|||
<field name="model_id" ref="model_galicea_git_repository" /> |
|||
<field name="groups" eval="[(4, ref('group_collaborator'))]"/> |
|||
<field name="domain_force"> |
|||
[('collaborator_ids', 'in', user.id)] |
|||
</field> |
|||
<field eval="1" name="perm_read" /> |
|||
<field eval="0" name="perm_write" /> |
|||
<field eval="0" name="perm_create" /> |
|||
<field eval="0" name="perm_unlink" /> |
|||
</record> |
|||
|
|||
<record id="repository_admin_access_rule" model="ir.rule"> |
|||
<field name="name">Administrators can access any repositories</field> |
|||
<field name="model_id" ref="model_galicea_git_repository" /> |
|||
<field name="groups" eval="[(4, ref('group_admin'))]"/> |
|||
<field name="domain_force"> |
|||
[(1, '=', 1)] |
|||
</field> |
|||
<field eval="1" name="perm_read" /> |
|||
<field eval="0" name="perm_write" /> |
|||
<field eval="0" name="perm_create" /> |
|||
<field eval="0" name="perm_unlink" /> |
|||
</record> |
|||
</odoo> |
After Width: 80 | Height: 80 | Size: 3.8 KiB |
After Width: 800 | Height: 354 | Size: 42 KiB |
After Width: 738 | Height: 359 | Size: 78 KiB |
After Width: 805 | Height: 273 | Size: 31 KiB |
@ -0,0 +1,19 @@ |
|||
<section class="oe_container"> |
|||
<div class="oe_row oe_spaced"> |
|||
<div class="oe_span12"> |
|||
<h2 class="oe_slogan">Galicea Git hosting</h2> |
|||
<h3 class="oe_slogan"> |
|||
Simple Odoo-based HTTP interface for Git repository hosting |
|||
</h3> |
|||
<p> |
|||
This add-on allows you to create Git repositories hosted by Odoo, and add specific Odoo users as collaborators. Only those users will have access to the repository. It requires <tt>git</tt> package, including <tt>git-http-backend</tt>, installed in the system. For Ubuntu/Debian it's enough to call |
|||
<pre>sudo apt install git</pre> |
|||
</p> |
|||
<h3>Creating repositories</h3> |
|||
<img class="oe_picture oe_screenshot" src="images/create_screenshot.png" /> |
|||
<img class="oe_picture oe_screenshot" src="images/config_screenshot.png" /> |
|||
<h3>Interacting with the repository</h3> |
|||
<img class="oe_picture oe_screenshot" src="images/console_screenshot.png" /> |
|||
</div> |
|||
</div> |
|||
</section> |
@ -0,0 +1,37 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import os |
|||
|
|||
from odoo.addons.galicea_environment_checkup import \ |
|||
custom_check, CheckWarning, CheckSuccess, CheckFail |
|||
from odoo import http |
|||
|
|||
@custom_check |
|||
def check_single_db(env): |
|||
if not http.request: |
|||
raise CheckWarning('Could not detect DB settings.') |
|||
|
|||
dbs = http.db_list(True, http.request.httprequest) |
|||
if len(dbs) == 1: |
|||
return CheckSuccess('Odoo runs in a single-DB mode.') |
|||
|
|||
details = ( |
|||
'<p>Odoo runs in a multi-DB mode, which will cause Git HTTP requests to fail.</p>' |
|||
'<p>Run Odoo with <tt>--dbfilter</tt> or <tt>--database</tt> flag.</p>' |
|||
) |
|||
return CheckFail( |
|||
'Odoo runs in a multi-DB mode.', |
|||
details=details |
|||
) |
|||
|
|||
@custom_check |
|||
def check_http_backend(env): |
|||
backend_path = env['ir.config_parameter'].sudo().get_param( |
|||
'galicea_git.git_http_backend' |
|||
) |
|||
if not os.access(backend_path, os.X_OK): |
|||
raise CheckFail( |
|||
'Git HTTP backend not found', |
|||
details='<a href="http://galicea.mw-odoo:8080/web#action=galicea_git.config_settings_action">Check the configuration here</a>' |
|||
) |
|||
return CheckSuccess('Git HTTP backend was found') |
@ -0,0 +1,66 @@ |
|||
<odoo> |
|||
<record id="repository_view_form" model="ir.ui.view"> |
|||
<field name="model">galicea_git.repository</field> |
|||
<field name="arch" type="xml"> |
|||
<form> |
|||
<group> |
|||
<field name="state" invisible="1" /> |
|||
<field name="name" /> |
|||
<field name="system_name" groups="galicea_git.group_admin" /> |
|||
<field name="collaborator_ids" widget="many2many_tags" options="{'no_create': True}" /> |
|||
</group> |
|||
<group class="oe_read_only"> |
|||
<label for="url" /> |
|||
<span style="font-family: monospace">git clone <field name="url" nolabel="True" /></span> |
|||
<field name="local_directory" style="font-family: monospace" /> |
|||
</group> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="repository_view_tree" model="ir.ui.view"> |
|||
<field name="model">galicea_git.repository</field> |
|||
<field name="arch" type="xml"> |
|||
<tree> |
|||
<field name="state" invisible="1" /> |
|||
<field name="name" /> |
|||
<field name="system_name" groups="galicea_git.group_admin" /> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<act_window id="repository_action" |
|||
name="Git repositories" |
|||
res_model="galicea_git.repository" /> |
|||
|
|||
<record id="config_settings_view_form" model="ir.ui.view"> |
|||
<field name="model">galicea_git.config.settings</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="Git hosting settings" class="oe_form_configuration"> |
|||
<header> |
|||
<button string="Save" type="object" name="execute" class="oe_highlight"/> |
|||
<button string="Cancel" type="object" name="cancel" class="oe_link"/> |
|||
</header> |
|||
<field name="git_http_backend_valid" invisible="1" /> |
|||
<group> |
|||
<label for="git_http_backend" /> |
|||
<span> |
|||
<field name="git_http_backend" nolabel="True" class="oe_inline" style="min-width:300px; margin-right:5px" /> |
|||
<i class="fa fa-check" aria-hidden="true" style="color: green" |
|||
attrs="{'invisible': [('git_http_backend_valid', '=', False)]}" /> |
|||
<i class="fa fa-times" aria-hidden="true" style="color: red" |
|||
attrs="{'invisible': [('git_http_backend_valid', '=', True)]}" /> |
|||
</span> |
|||
</group> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<act_window id="config_settings_action" name="Settings" res_model="galicea_git.config.settings" |
|||
view_mode="form" target="inline" /> |
|||
|
|||
<menuitem name="Git hosting" id="root_menu" sequence="20" /> |
|||
<menuitem name="Repositories" id="repo_menu" parent="galicea_git.root_menu" action="repository_action" sequence="1" /> |
|||
<menuitem name="Settings" id="settings_menu" parent="galicea_git.root_menu" action="config_settings_action" sequence="99" |
|||
groups="galicea_git.group_admin" /> |
|||
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue