Compare commits

...

5 Commits
12.0 ... 16.0

  1. 1
      galicea_base/README.md
  2. 2
      galicea_base/__init__.py
  3. 23
      galicea_base/__manifest__.py
  4. 6
      galicea_base/views/base_menu.xml
  5. 1
      galicea_environment_checkup/README.md
  6. 9
      galicea_environment_checkup/__init__.py
  7. 32
      galicea_environment_checkup/__manifest__.py
  8. 3
      galicea_environment_checkup/controllers/__init__.py
  9. 26
      galicea_environment_checkup/controllers/dashboard.py
  10. 4
      galicea_environment_checkup/environment_checkup/__init__.py
  11. 49
      galicea_environment_checkup/environment_checkup/core.py
  12. 30
      galicea_environment_checkup/environment_checkup/custom.py
  13. 183
      galicea_environment_checkup/environment_checkup/dependencies.py
  14. 24
      galicea_environment_checkup/environment_checkup/runtime.py
  15. 0
      galicea_environment_checkup/environment_checkup/utils.py
  16. 1
      galicea_environment_checkup/models/__init__.py
  17. 19
      galicea_environment_checkup/models/ext_module.py
  18. BIN
      galicea_environment_checkup/static/description/icon.png
  19. BIN
      galicea_environment_checkup/static/description/images/custom_screenshot.png
  20. BIN
      galicea_environment_checkup/static/description/images/dependencies_screenshot.png
  21. 79
      galicea_environment_checkup/static/description/index.html
  22. 146
      galicea_environment_checkup/static/src/js/environment_checkup.js
  23. 108
      galicea_environment_checkup/static/src/js/environment_checkup10.js
  24. 71
      galicea_environment_checkup/static/src/xml/templates.xml
  25. 9
      galicea_environment_checkup/views/data.xml
  26. 17
      galicea_environment_checkup/views/environment_checks.xml
  27. 20
      galicea_environment_checkup/views/views.xml
  28. 1
      galicea_git/README.md
  29. 5
      galicea_git/__init__.py
  30. 48
      galicea_git/__manifest__.py
  31. 3
      galicea_git/controllers/__init__.py
  32. 86
      galicea_git/controllers/main.py
  33. 12
      galicea_git/data/config.xml
  34. 63
      galicea_git/http_chunked_fix.py
  35. 4
      galicea_git/models/__init__.py
  36. 42
      galicea_git/models/config_settings.py
  37. 105
      galicea_git/models/repository.py
  38. 3
      galicea_git/security/ir.model.access.csv
  39. 47
      galicea_git/security/security.xml
  40. BIN
      galicea_git/static/description/icon.png
  41. BIN
      galicea_git/static/description/images/config_screenshot.png
  42. BIN
      galicea_git/static/description/images/console_screenshot.png
  43. BIN
      galicea_git/static/description/images/create_screenshot.png
  44. 19
      galicea_git/static/description/index.html
  45. 37
      galicea_git/system_checks.py
  46. 71
      galicea_git/views/views.org.xml
  47. 43
      galicea_git/views/views.xml
  48. 1
      galicea_git_oauth/README.md
  49. 4
      galicea_git_oauth/__init__.py
  50. 21
      galicea_git_oauth/__manifest__.py
  51. 1
      galicea_git_oauth/controllers/__init__.py
  52. 14
      galicea_git_oauth/controllers/ext_git_main.py
  53. 1
      galicea_git_oauth/models/__init__.py
  54. 24
      galicea_git_oauth/models/ext_repository.py
  55. 5
      galicea_openapi/__init__.py
  56. 33
      galicea_openapi/__manifest__.py
  57. 4
      galicea_openapi/controllers/__init__.py
  58. 56
      galicea_openapi/controllers/api.py
  59. 22
      galicea_openapi/doc/helloworld.py
  60. 41
      galicea_openapi/doc/test1.py
  61. 3
      galicea_openapi/models/__init__.py
  62. 52
      galicea_openapi/openapi.py
  63. 2
      galicea_openapi/security/ir.model.access.csv
  64. BIN
      galicea_openapi/static/description/icon.png
  65. 1
      galicea_openid_connect/__init__.py
  66. 71
      galicea_openid_connect/__manifest__.py
  67. 23
      galicea_openid_connect/api.py
  68. 7
      galicea_openid_connect/controllers/ext_web_login.py
  69. 111
      galicea_openid_connect/controllers/main.py
  70. 57
      galicea_openid_connect/models/client.py
  71. 45
      galicea_openid_connect/models/config_parameter.py
  72. 33
      galicea_openid_connect/security/__init__.py
  73. 24
      galicea_openid_connect/system_checks.py
  74. 22
      galicea_openid_connect/views/views.xml
  75. 30
      galicea_toolset/README.md
  76. 1
      galicea_toolset/__init__.py
  77. 20
      galicea_toolset/__manifest__.py
  78. 1
      galicea_toolset/static/src/js/.#one2many_flexible_widget.js
  79. 38
      galicea_toolset/static/src/js/client_actions.js
  80. 49
      galicea_toolset/static/src/js/iframe_widget.js
  81. 68
      galicea_toolset/static/src/js/one2many_flexible_widget.js
  82. 16
      galicea_toolset/utils.py
  83. 11
      galicea_toolset/views/data.xml

1
galicea_base/README.md

@ -1 +0,0 @@
Base menu for Odoo Galicea Ecosystem

2
galicea_base/__init__.py

@ -1,2 +0,0 @@
# -*- coding: utf-8 -*-

23
galicea_base/__manifest__.py

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': "Base menu for Odoo Galicea Ecosystem",
'summary': """
Menu only
""",
'author': "Jurek Wawro",
'maintainer': "Galicea",
'website': "http://galicea.pl",
'category': 'Technical Settings',
'version': '12.0.1.0',
'depends': ['web',],
'data': [
'views/base_menu.xml',
],
'installable': True
}

6
galicea_base/views/base_menu.xml

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem name="Galicea" id="galicea_admin_menu"
parent="base.menu_administration" groups="base.group_erp_manager" />
</odoo>

1
galicea_environment_checkup/README.md

@ -1 +0,0 @@
[See add-on page on odoo.com](https://apps.odoo.com/apps/modules/10.0/galicea_environment_checkup/)

9
galicea_environment_checkup/__init__.py

@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
from . import models
from .environment_checkup.custom import custom_check
from .environment_checkup.core import CheckFail, CheckWarning, CheckSuccess
from . import controllers

32
galicea_environment_checkup/__manifest__.py

@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': "Galicea Environment Check-up",
'summary': """
Programmatically validate environment, including internal and external
dependencies""",
'author': "Maciej Wawro",
'maintainer': "Galicea",
'website': "http://galicea.pl",
'category': 'Technical Settings',
'version': '12.0.1.0',
'depends': ['web','galicea_base',],
'data': [
'views/data.xml',
'views/views.xml',
'views/environment_checks.xml'
],
'qweb': ['static/src/xml/templates.xml'],
'images': [
'static/description/images/custom_screenshot.png',
'static/description/images/dependencies_screenshot.png'
],
'installable': True
}

3
galicea_environment_checkup/controllers/__init__.py

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import dashboard

26
galicea_environment_checkup/controllers/dashboard.py

@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import http
from odoo.exceptions import AccessError
from odoo.http import request
from ..environment_checkup.runtime import all_installed_checks, display_data
from ..environment_checkup.core import CheckResult
class Dashboard(http.Controller):
@http.route('/galicea_environment_checkup/data', type='json', auth='user')
def data(self, request, **kw):
if not request.env.user.has_group('base.group_erp_manager'):
raise AccessError("Access Denied")
checks = all_installed_checks(request.env)
response = display_data(request.env, checks)
priority = {
CheckResult.FAIL: 0,
CheckResult.WARNING: 1,
CheckResult.SUCCESS: 2
}
response.sort(key=lambda res: (priority[res['result']], res['module']))
return response

4
galicea_environment_checkup/environment_checkup/__init__.py

@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
from . import core
from . import custom

49
galicea_environment_checkup/environment_checkup/core.py

@ -1,49 +0,0 @@
# -*- coding: utf-8 -*-
import logging
_logger = logging.getLogger(__name__)
class CheckResult(object):
SUCCESS = 'success'
WARNING = 'warning'
FAIL = 'fail'
def __init__(self, result, message, details = None):
super(CheckResult, self).__init__()
self.result = result
self.message = message
self.details = details
class CheckSuccess(CheckResult):
def __init__(self, message, **kwargs):
super(CheckSuccess, self).__init__(CheckResult.SUCCESS, message, **kwargs)
class CheckIssue(CheckResult, Exception):
def __init__(self, result, message, **kwargs):
Exception.__init__(self, message)
CheckResult.__init__(self, result, message, **kwargs)
class CheckFail(CheckIssue):
def __init__(self, message, **kwargs):
super(CheckFail, self).__init__(CheckResult.FAIL, message, **kwargs)
class CheckWarning(CheckIssue):
def __init__(self, message, **kwargs):
super(CheckWarning, self).__init__(CheckResult.WARNING, message, **kwargs)
class Check(object):
def __init__(self, module):
self.module = module
def run(self, env):
try:
return self._run(env)
except CheckIssue as issue:
return issue
except Exception as ex:
_logger.exception(ex)
return CheckFail('Check failed when processing: {}'.format(ex))
def _run(self, env):
raise NotImplementedError('Should be overriden by the subclass')

30
galicea_environment_checkup/environment_checkup/custom.py

@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
import collections
from .core import Check
custom_checks_per_module = collections.defaultdict(list)
class CustomCheck(Check):
def __init__(self, module, func):
super(CustomCheck, self).__init__(module)
self.func = func
def _run(self, env):
return self.func(env)
def custom_check(func):
try:
module = func.__module__.split('.')[2]
except IndexError:
module = ''
custom_checks_per_module[module].append(
CustomCheck(module=module, func=func)
)
return func
def get_checks_for_module(module_name):
return custom_checks_per_module[module_name]

183
galicea_environment_checkup/environment_checkup/dependencies.py

@ -1,183 +0,0 @@
# -*- coding: utf-8 -*-
import subprocess
import re
import cgi
from odoo.modules.module import load_information_from_description_file
from odoo.tools import which
from .core import Check, CheckSuccess, CheckWarning, CheckFail
class DependencyCheck(Check):
dependency_type = None
def __init__(self, module, dependency):
super(DependencyCheck, self).__init__(module)
self.dependency = dependency
def _dependency_installed(self, env, name):
raise NotImplementedError('Should be overriden by the subclass')
def _installed_version(self, env, name):
raise NotImplementedError('Should be overriden by the subclass')
def _details(self):
if 'install' in self.dependency:
return 'Install command: <pre>{}</pre>'.format(self.dependency['install'])
return None
def __has_required_version(self, installed_version, version_expression):
version_operator = '='
version = self.dependency['version']
if version[:1] in ['=', '~', '^']:
version_operator = version[:1]
version = version[1:]
elif version[:2] in ['>=']:
version_operator = version[:2]
version = version[2:]
# Py3 : map -> list(map
# https://stackoverflow.com/questions/33717314/attributeerror-map-obejct-has-no-attribute-index-python-3
try:
parsed_version = list(map(int, version.split('.')))
except ValueError:
raise CheckFail(
'Invalid version expression',
details = """
Allowed expressions are <pre>=x.y.z</pre>, <pre>&gt;=x.y.z</pre>, <pre>^x.z.y</pre>,
<pre>~x.y.z. Got <pre>{}</pre>""".format(cgi.escape(self.dependency['version']))
)
parsed_installed_version = list(map(int, installed_version.split('.')))
parsed_version.extend(0 for _ in range(len(parsed_installed_version) - len(parsed_version)))
parsed_installed_version.extend(0 for _ in range(len(parsed_version) - len(parsed_installed_version)))
if version_operator == '^':
if parsed_installed_version[:1] != parsed_version[:1]:
return False
version_operator = '>='
elif version_operator == '~':
if parsed_installed_version[:2] != parsed_version[:2]:
return False
version_operator = '>='
if version_operator == '>=':
return tuple(parsed_installed_version) >= tuple(parsed_version)
elif version_operator == '=':
return tuple(parsed_installed_version) == tuple(parsed_version)
assert False
def _run(self, env):
name = self.dependency['name']
if not self._dependency_installed(env, name):
raise CheckFail(
'Required {} - {} - is not installed.'.format(self.dependency_type, name),
details=self._details()
)
if 'version' in self.dependency:
version_expression = self.dependency['version']
installed_version = self._installed_version(env, name)
if not self.__has_required_version(installed_version, version_expression):
raise CheckWarning(
'Required {} - {} - has version {}, but {} is needed.'.format(
self.dependency_type,
name,
installed_version,
version_expression
),
details=self._details()
)
return CheckSuccess(
'Required {} - {} - is installed.'.format(self.dependency_type, name),
details=self._details()
)
class InternalDependencyCheck(DependencyCheck):
dependency_type = 'Odoo module'
def _dependency_installed(self, env, name):
return name in env.registry._init_modules
def _installed_version(self, env, name):
return env['ir.module.module'].sudo().search([('name', '=', name)]).latest_version
class PythonDependencyCheck(DependencyCheck):
dependency_type = 'Python module'
def _dependency_installed(self, env, name):
try:
__import__(name)
return True
except ImportError:
return False
def _installed_version(self, env, name):
try:
return __import__(name).__version__
except AttributeError:
raise CheckWarning(
'Could not detect version of the Python module: {}.'.format(name),
details=self._details()
)
class ExternalDependencyCheck(DependencyCheck):
dependency_type = 'system executable'
def _dependency_installed(self, env, name):
try:
which(name)
return True
except IOError:
return False
def _installed_version(self, env, name):
try:
exe = which(name)
out = str(subprocess.check_output([exe, '--version'])) # Py3 str()
match = re.search('[\d.]+', out)
if not match:
raise CheckWarning(
'Unable to detect version for executable {}'.format(name),
details="Command {} --version returned <pre>{}</pre>".format(exe, out)
)
return match.group(0)
except subprocess.CalledProcessError as e:
raise CheckWarning(
'Unable to detect version for executable {}: {}'.format(name, e),
details=self._details()
)
def get_checks_for_module(module_name):
result = []
manifest = load_information_from_description_file(module_name)
manifest_checks = manifest.get('environment_checkup') or {}
dependencies = manifest_checks.get('dependencies') or {}
for dependency in dependencies.get('python') or []:
result.append(PythonDependencyCheck(module_name, dependency))
for dependency in dependencies.get('external') or []:
result.append(ExternalDependencyCheck(module_name, dependency))
for dependency in dependencies.get('internal') or []:
result.append(InternalDependencyCheck(module_name, dependency))
return result
def get_checks_for_module_recursive(module):
class ModuleDFS(object):
def __init__(self):
self.visited_modules = set()
self.checks = []
def visit(self, module):
if module.name in self.visited_modules:
return
self.visited_modules.add(module.name)
self.checks += get_checks_for_module(module.name)
for module_dependency in module.dependencies_id:
if module_dependency.depend_id:
self.visit(module_dependency.depend_id)
return self
return ModuleDFS().visit(module).checks

24
galicea_environment_checkup/environment_checkup/runtime.py

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
from . import custom, dependencies
def all_installed_checks(env):
result = []
installed_modules = env.registry._init_modules
for module_name in installed_modules:
result += custom.get_checks_for_module(module_name)
result += dependencies.get_checks_for_module(module_name)
return result
def display_data(env, checks):
response = []
for check in checks:
result = check.run(env)
response.append({
'module': check.module,
'message': result.message,
'details': result.details,
'result': result.result
})
return response

0
galicea_environment_checkup/environment_checkup/utils.py

1
galicea_environment_checkup/models/__init__.py

@ -1 +0,0 @@
from . import ext_module

19
galicea_environment_checkup/models/ext_module.py

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
import json
from odoo import api, fields, models
from ..environment_checkup import dependencies
from ..environment_checkup.runtime import display_data
class Module(models.Model):
_inherit = 'ir.module.module'
dependency_checks = fields.Text(
compute='_compute_dependency_checks'
)
@api.one
def _compute_dependency_checks(self):
checks = dependencies.get_checks_for_module_recursive(self)
self.dependency_checks = json.dumps(display_data(self.env, checks))

BIN
galicea_environment_checkup/static/description/icon.png

Before

Width: 80  |  Height: 80  |  Size: 3.4 KiB

BIN
galicea_environment_checkup/static/description/images/custom_screenshot.png

Before

Width: 625  |  Height: 198  |  Size: 24 KiB

BIN
galicea_environment_checkup/static/description/images/dependencies_screenshot.png

Before

Width: 966  |  Height: 755  |  Size: 92 KiB

79
galicea_environment_checkup/static/description/index.html

@ -1,79 +0,0 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan">Galicea Environment Check-up</h2>
<h3 class="oe_slogan">
Programmatically validate Odoo environment, including internal and external dependencies of your add-on
</h3>
This add-on allows you to:
<ul>
<li>programmatically check software dependencies required by your add-on, as well as inform the Administrator as to how to meet them,</li>
<li>add custom verification for Odoo instance set-up and inform the Administrator about any inconsistencies.</li>
</ul>
<h2>Add-on dependency verification</h2>
<img class="oe_picture oe_screenshot" src="images/dependencies_screenshot.png" />
<h3>How-to</h3>
Just add <tt>'environment_checkup'</tt> entry to <tt>__manifest__.py</tt>.
<pre>
{
...
'environment_checkup': {
'dependencies': {
'python': [
{
'name': 'Crypto',
'version': '>=2.6.2',
'install': "pip install 'PyCrypto>=2.6.1'"
},
],
'external': [
{
'name': 'wkhtmltopdf',
'install': "apt install wkhtmltopdf"
},
{
'name': 'git',
'version': '^3.0.0',
'install': "apt install git"
}
],
'internal': [
{
'name': 'web',
'version': '~10.0.1.0'
}
]
}
}
}
</pre>
<h2>Custom environment verification</h2>
<img class="oe_picture oe_screenshot" src="images/custom_screenshot.png" />
<h3>How-to</h3>
1. Add the check, e.g. in the <tt>system_checks.py</tt> file:
<pre>
# -*- coding: utf-8 -*-
import cgi
from odoo.addons.galicea_environment_checkup import custom_check, CheckSuccess, CheckWarning, CheckFail
@custom_check
def check_mail(env):
users_without_emails = env['res.users'].sudo().search([('email', '=', False)])
if users_without_emails:
raise CheckWarning(
'Some users don\'t have their e-mails set up.',
details='See user <tt>{}</tt>.'.format(cgi.escape(users_without_emails[0].name))
)
return CheckSuccess('All users have their e-mails set.')
</pre>
2. Make sure it's loaded by <tt>__init__.py</tt>
<pre>
# -*- coding: utf-8 -*-
from . import system_checks
</pre>
</div>
</div>
</section>

146
galicea_environment_checkup/static/src/js/environment_checkup.js

@ -1,146 +0,0 @@
odoo.define('galicea_environment_checkup', function (require) {
"use strict";
//var SystrayMenu = require('web.SystrayMenu');
//var Model = require('web.Model');
var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
//var framework = require('web.framework');
var session = require('web.session');
//var Widget = require('web.Widget');
//////////////////
var QWeb = core.qweb;
//var _t = core._t;
/* SystrayIcon - nie działa poprawnie ???
//https://www.odoo.com/documentation/12.0/reference/javascript_reference.html
var Model = require('web.Model');
var Users = new Model('res.users');
var SystrayIcon = Widget.extend({
tagName: 'li',
events: {
"click": "on_click",
},
start: function(){
this.load(this.all_dashboards);
return this._super();
},
load: function(dashboards){
var self = this;
var loading_done = new $.Deferred();
Users.call('has_group', ['base.group_erp_manager']).then(function(is_admin) {
if (is_admin) {
session.rpc('/galicea_environment_checkup/data', {})
.then(function (data) {
var counts = { 'success': 0, 'warning': 0, 'fail': 0 };
data.forEach(function (check) { ++counts[check.result]; });
var result;
if (counts['fail']) {
result = 'fail';
} else if (counts['warning']) {
result = 'warning';
} else {
result = 'success';
}
self.replaceElement(QWeb.render('GaliceaEnvironmentCheckupIcon', {
'result': result,
'count': counts['warning'] + counts['fail']
}));
loading_done.resolve();
});
} else {
loading_done.resolve();
}
});
return loading_done;
},
on_click: function (event) {
event.preventDefault();
this.do_action('galicea_environment_checkup.dashboard_action', {clear_breadcrumbs: true});
},
});
*/
/////////////////////////////
var Dashboard = AbstractAction.extend({
// v.10 var Dashboard = Widget.extend({
start: function(){
return this.load(this.all_dashboards);
},
load: function(dashboards) {
var self = this;
var loading_done = new $.Deferred();
session.rpc('/galicea_environment_checkup/data', {})
.then(function (data) {
self._replaceElement(QWeb.render('GaliceaEnvironmentCheckupDashboard', {'data': data})); // v.10: self.replaceElement
loading_done.resolve();
});
return loading_done;
},
});
//!JW - nowa propozycja: core.action_registry.add('galicea_environment_checkup.environment_checkup', Dashboard);
core.action_registry.add('galicea_environment_checkup.dashboard', Dashboard);
////////////////////
/* v.10
var FormWidget = form_common.AbstractField.extend({
init: function() {
this._super.apply(this, arguments);
this.set("value", "[]");
},
render_value: function() {
var data = JSON.parse(this.get('value'));
if (data.length == 0) {
this.replaceElement('<div />');
return;
}
this.replaceElement(QWeb.render('GaliceaEnvironmentCheckupFormWidget', {'data': data}));
},
});
core.form_widget_registry.add('environment_checks', FormWidget);
*/
var FormView = require('web.FormView');
var FormWidget = FormView.extend({
template: "environment_checks",
init: function() {
this._super.apply(this, arguments);
this.set("value", "[]");
},
events: {
},
render_value: function() {
var data = JSON.parse(this.get('value'));
if (data.length == 0) {
this._replaceElement('<div />');
return;
}
this._replaceElement(QWeb.render('GaliceaEnvironmentCheckupFormWidget', {'data': data}));
}
});
////////////////////
return {
//!! SystrayIcon: SystrayIcon,
Dashboard: Dashboard,
FormWidget: FormWidget
};
});

108
galicea_environment_checkup/static/src/js/environment_checkup10.js

@ -1,108 +0,0 @@
odoo.define('galicea_environment_checkup', function(require) {
"use strict";
var core = require('web.core');
var form_common = require('web.form_common');
var Widget = require('web.Widget');
var session = require('web.session');
var QWeb = core.qweb;
var SystrayMenu = require('web.SystrayMenu');
var Model = require('web.Model');
var Users = new Model('res.users');
var SystrayIcon = Widget.extend({
tagName: 'li',
events: {
"click": "on_click",
},
start: function(){
this.load(this.all_dashboards);
return this._super();
},
load: function(dashboards){
var self = this;
var loading_done = new $.Deferred();
Users.call('has_group', ['base.group_erp_manager']).then(function(is_admin) {
if (is_admin) {
session.rpc('/galicea_environment_checkup/data', {})
.then(function (data) {
var counts = { 'success': 0, 'warning': 0, 'fail': 0 };
data.forEach(function (check) { ++counts[check.result]; });
var result;
if (counts['fail']) {
result = 'fail';
} else if (counts['warning']) {
result = 'warning';
} else {
result = 'success';
}
self.replaceElement(QWeb.render('GaliceaEnvironmentCheckupIcon', {
'result': result,
'count': counts['warning'] + counts['fail']
}));
loading_done.resolve();
});
} else {
loading_done.resolve();
}
});
return loading_done;
},
on_click: function (event) {
event.preventDefault();
this.do_action('galicea_environment_checkup.dashboard_action', {clear_breadcrumbs: true});
},
});
SystrayMenu.Items.push(SystrayIcon);
var Dashboard = Widget.extend({
start: function(){
return this.load(this.all_dashboards);
},
load: function(dashboards) {
var self = this;
var loading_done = new $.Deferred();
session.rpc('/galicea_environment_checkup/data', {})
.then(function (data) {
self.replaceElement(QWeb.render('GaliceaEnvironmentCheckupDashboard', {'data': data}));
loading_done.resolve();
});
return loading_done;
},
});
core.action_registry.add('galicea_environment_checkup.dashboard', Dashboard);
var FormWidget = form_common.AbstractField.extend({
init: function() {
this._super.apply(this, arguments);
this.set("value", "[]");
},
render_value: function() {
var data = JSON.parse(this.get('value'));
if (data.length == 0) {
this.replaceElement('<div />');
return;
}
this.replaceElement(QWeb.render('GaliceaEnvironmentCheckupFormWidget', {'data': data}));
},
});
core.form_widget_registry.add('environment_checks', FormWidget);
return {
SystrayIcon: SystrayIcon,
Dashboard: Dashboard,
FormWidget: FormWidget
};
});

71
galicea_environment_checkup/static/src/xml/templates.xml

@ -1,71 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="GaliceaEnvironmentCheckupIcon">
<li>
<a href="#" t-att-title="result == 'success' ? 'No system setup issues' : 'Found '+count+' system setup issues'">
<t t-if="result == 'success'">
<i class="fa fa-check" style="opacity:0.5"></i>
</t>
<t t-if="result == 'warning'">
<i class="fa fa-exclamation-triangle" style="color: orange"></i>
</t>
<t t-if="result == 'fail'">
<i class="fa fa-exclamation-circle" style="color: red"></i>
</t>
<t t-if="result != 'success'">
<span><t t-raw="count" /></span>
</t>
</a>
</li>
</t>
<t t-name="GaliceaEnvironmentChecks">
<div class="row">
<t t-foreach="data" t-as="check">
<div class="col-md-12">
<div style="display: flex; background-color: white; padding: 10px; margin: 10px">
<div style="flex-grow: 0; flex-shrink: 0; width:50px; margin-right: 20px">
<t t-if="check.result == 'success'">
<i class="fa fa-check fa-4x" style="color: green" ></i>
</t>
<t t-if="check.result == 'warning'">
<i class="fa fa-exclamation-triangle fa-4x" style="color: orange" ></i>
</t>
<t t-if="check.result == 'fail'">
<i class="fa fa-exclamation-circle fa-4x" style="color: red" ></i>
</t>
</div>
<div style="flex-grow: 1; flex-shrink: 1">
Module: <t t-esc="check.module" />
<h4><t t-esc="check.message" /></h4>
<div>
<t t-raw="check.details" />
</div>
</div>
</div>
</div>
</t>
</div>
</t>
<t t-name="GaliceaEnvironmentCheckupDashboard">
<div class="container">
<div class="row">
<div class="col-md-12">
<h2>Environment check-up</h2>
<t t-call="GaliceaEnvironmentChecks">
<t t-set="data" t-value="data" />
</t>
</div>
</div>
</div>
</t>
<t t-name="GaliceaEnvironmentCheckupFormWidget">
<h1>Module dependencies</h1>
<t t-call="GaliceaEnvironmentChecks">
<t t-set="data" t-value="data" />
</t>
</t>
</templates>

9
galicea_environment_checkup/views/data.xml

@ -1,9 +0,0 @@
<odoo>
<data>
<template id="assets_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script src="/galicea_environment_checkup/static/src/js/environment_checkup.js" type="text/javascript" />
</xpath>
</template>
</data>
</odoo>

17
galicea_environment_checkup/views/environment_checks.xml

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="stale_modules">
<p>The following modules need to be upgraded:</p>
<ul>
<t t-foreach="modules" t-as="module">
<li>
<a t-att-href="'#id={}&amp;view_type=form&amp;model=ir.module.module'.format(module.id)">
<t t-esc="module.shortdesc" /> (technical name: <t t-esc="module.name" />;
version on disk: <strong><t t-esc="module.installed_version" /></strong>;
version in DB: <strong><t t-esc="module.latest_version" /></strong>)
</a>
</li>
</t>
</ul>
</template>
</odoo>

20
galicea_environment_checkup/views/views.xml

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="dashboard_action" model="ir.actions.client">
<field name="name">Environment check-up dashboard</field>
<field name="tag">galicea_environment_checkup.dashboard</field>
</record>
<menuitem name="Environment check-up" id="dashboard_menu"
action="dashboard_action" parent="galicea_base.galicea_admin_menu" groups="base.group_erp_manager" />
<record id="module_form" model="ir.ui.view">
<field name="name">module_form.checks</field>
<field name="model">ir.module.module</field>
<field name="inherit_id" ref="base.module_form"/>
<field name="arch" type="xml">
<field name="description_html" position="before">
<field name="dependency_checks" widget="environment_checks" />
</field>
</field>
</record>
</odoo>

1
galicea_git/README.md

@ -1 +0,0 @@
[See add-on page on odoo.com](https://apps.odoo.com/apps/modules/10.0/galicea_git/)

5
galicea_git/__init__.py

@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import system_checks

48
galicea_git/__manifest__.py

@ -1,48 +0,0 @@
# -*- 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': '12.0.0.2',
'depends': ['web', 'galicea_environment_checkup','galicea_base'],
'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"
}
]
}
}
}

3
galicea_git/controllers/__init__.py

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import main

86
galicea_git/controllers/main.py

@ -1,86 +0,0 @@
# -*- 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):
def authorize(self, request):
auth = request.httprequest.authorization
if auth:
request.session.authenticate(request.session.db, auth.username, auth.password)
@http.route(
[
'/git/<repo>',
'/git/<repo>/<path:path>',
],
auth='public',
csrf=False
)
def git(self, request, repo, **kw):
self.authorize(request)
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(b"\r\n\r\n", 2)
http_status_code = 200
headers = []
for header in headers_str.split(b"\r\n"):
name, value = header.split(b': ', 2)
if name == 'Status':
http_code = int(value.split(b' ')[0])
else:
headers.append((name.decode('ascii'), value.decode('ascii')))
return werkzeug.Response(
body,
status = http_status_code,
headers = headers
)

12
galicea_git/data/config.xml

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<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>

63
galicea_git/http_chunked_fix.py

@ -1,63 +0,0 @@
# -*- 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

4
galicea_git/models/__init__.py

@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
from . import repository
from . import config_settings

42
galicea_git/models/config_settings.py

@ -1,42 +0,0 @@
# -*- 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]

105
galicea_git/models/repository.py

@ -1,105 +0,0 @@
# -*- coding: utf-8 -*-
import os
import random
import shutil
import string
import subprocess
try:
import git
except ImportError:
pass
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])

3
galicea_git/security/ir.model.access.csv

@ -1,3 +0,0 @@
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

47
galicea_git/security/security.xml

@ -1,47 +0,0 @@
<?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>

BIN
galicea_git/static/description/icon.png

Before

Width: 80  |  Height: 80  |  Size: 3.8 KiB

BIN
galicea_git/static/description/images/config_screenshot.png

Before

Width: 800  |  Height: 354  |  Size: 42 KiB

BIN
galicea_git/static/description/images/console_screenshot.png

Before

Width: 738  |  Height: 359  |  Size: 78 KiB

BIN
galicea_git/static/description/images/create_screenshot.png

Before

Width: 805  |  Height: 273  |  Size: 31 KiB

19
galicea_git/static/description/index.html

@ -1,19 +0,0 @@
<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>

37
galicea_git/system_checks.py

@ -1,37 +0,0 @@
# -*- 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')

71
galicea_git/views/views.org.xml

@ -1,71 +0,0 @@
<odoo>
<data>
<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"
parent="base.menu_administration"
view_mode="form" target="inline" />
<menuitem name="Git hosting" id="git_root_menu" sequence="20" />
<menuitem name="Repositories" id="repo_menu" parent="galicea_git.git_root_menu" action="repository_action" sequence="1" />
<menuitem name="Settings" id="settings_menu" parent="galicea_git.git_root_menu" action="config_settings_action" sequence="99"
groups="galicea_git.group_admin" />
</data>
</odoo>

43
galicea_git/views/views.xml

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<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" />
<menuitem name="Repositories" id="repo_menu" parent="galicea_base.galicea_admin_menu"
action="repository_action" sequence="1" />
</data>
</odoo>

1
galicea_git_oauth/README.md

@ -1 +0,0 @@
[See add-on page on odoo.com](https://apps.odoo.com/apps/modules/12.0/galicea_git_oauth/)

4
galicea_git_oauth/__init__.py

@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
from . import models
from . import controllers

21
galicea_git_oauth/__manifest__.py

@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': "Galicea Git OAuth",
'summary': """
Enables Git auth via OAuth token""",
'author': "Maciej Wawro",
'maintainer': "Galicea",
'website': "http://galicea.pl",
'category': 'Technical Settings',
'version': '12.0.1.0',
'depends': ['galicea_git', 'galicea_openid_connect'],
'data': [
],
'installable': True
}

1
galicea_git_oauth/controllers/__init__.py

@ -1 +0,0 @@
from . import ext_git_main

14
galicea_git_oauth/controllers/ext_git_main.py

@ -1,14 +0,0 @@
from odoo.addons.galicea_git.controllers.main import Main
class ExtMain(Main):
def authorize(self, req):
auth = req.httprequest.authorization
if auth and auth.password == 'bearer':
access_token = req.httprequest.authorization.username
token = req.env['galicea_openid_connect.access_token'].sudo().search(
[('token', '=', access_token)]
)
if token:
req.uid = token.user_id.id
return
super(ExtMain, self).authorize(req)

1
galicea_git_oauth/models/__init__.py

@ -1 +0,0 @@
from . import ext_repository

24
galicea_git_oauth/models/ext_repository.py

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
from urllib.parse import urlparse
from odoo import models
class Repository(models.Model):
_inherit = 'galicea_git.repository'
def authenticated_url(self, client):
"""
@param application galicea_openid.application"""
token = self.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create(
self.env.user.id,
client.id
)
unauthenticated_url = self.url
url_parts = urlparse(unauthenticated_url)
return '{}://{}:bearer@{}{}'.format(
url_parts.scheme,
token.token,
url_parts.netloc,
url_parts.path,
)

5
galicea_openapi/__init__.py

@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
#from . import models
from . import controllers

33
galicea_openapi/__manifest__.py

@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': "openapi",
'summary': """
Odoo Opnapi
UWAGA! Obecnie dekorator apiroute ma ograniczoną funkcjonalność.
M.in. tylko jeden URL
controllers/api.py zawiera przykład wykorzystania -
pod adresem /oapi/api zwraca dokumentację w JSON
""",
'description': """
""",
'author': 'Jerzy Wawro',
'maintainer': "Galicea",
'website': "http://www.galicea.pl",
'category': 'Tools',
'version': '12.0.0.1',
'depends': [
],
'external_dependencies': {
'python': [ 'fastapi', 'pydantic', 'starlette' ]
},
'data': [
],
'application': True,
'installable': True,
}

4
galicea_openapi/controllers/__init__.py

@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
from . import api

56
galicea_openapi/controllers/api.py

@ -1,56 +0,0 @@
# -*- coding: utf-8 -*-
import json
from fastapi.openapi.docs import get_swagger_ui_html
from odoo import http, _
from ..openapi import apiroute
from ..openapi import oapi
class OpenApiTest(http.Controller):
@http.route(['/oapi/tst1',], type='http', auth="user", website=True)
def tst1(self, **kw):
return "tst1"
@oapi.get('/oapi/tst2')
@http.route(['/oapi/tst2',], type='http', auth="user", website=True)
def tst2(self):
return 'ok test2'
@oapi.api_route('/oapi/tst3')
@http.route(['/oapi/tst3',], type='http', auth="user", website=True)
def tst3(self, par1="abc"):
return par1
@oapi.api_route('/oapi/tst4')
@http.route(['/oapi/tst4', ], type='http', auth="user", website=True)
def tst4(self,par1="444"):
return par1
@apiroute('/oapi/tst5')
def tst5(self, par1="555"):
return par1
@http.route(['/oapi/api',], type='http', auth="user", website=True)
def api(self, **kw):
return json.dumps(oapi.openapi())
# wynik możesz skopiować do https://editor.swagger.io/
@http.route(['/oapi/docs',], type='http', auth="user", website=True)
def api_UI(self, **kw):
response = get_swagger_ui_html(openapi_url = '/oapi/api', title = 'tytuł')
return response.body

22
galicea_openapi/doc/helloworld.py

@ -1,22 +0,0 @@
# pip install fastapi
# pip install email-validator
# pip install pydantic
# pip install starlette
# pip install uvicorn
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
def run_server():
uvicorn.run(app)
if __name__ == '__main__':
uvicorn.run(app, #'server:app',
host='127.0.0.1', port=8000, reload=True)

41
galicea_openapi/doc/test1.py

@ -1,41 +0,0 @@
# pip install fastapi
# pip install email-validator
# pip install pydantic
# pip install starlette
# pip install uvicorn
import uvicorn
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
app = FastAPI()
@app.get("/items/")
async def read_items():
return [{"name": "Foo"}]
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="Custom title",
version="2.5.0",
description="This is a very custom OpenAPI schema",
routes=app.routes,
)
openapi_schema["info"]["x-logo"] = {
"url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"
}
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
if __name__ == '__main__':
print("see http://127.0.0.1:8000/docs")
uvicorn.run(app, #'server:app',
host='127.0.0.1', port=8000, reload=True)

3
galicea_openapi/models/__init__.py

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-

52
galicea_openapi/openapi.py

@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
import functools
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
def custom_openapi():
if oapi.openapi_schema:
return oapi.openapi_schema
openapi_schema = get_openapi(
title="Custom title",
version="2.5.0",
description="This is a very custom OpenAPI schema",
routes=oapi.routes,
)
openapi_schema["info"]["x-logo"] = {
"url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"
}
oapi.openapi_schema = openapi_schema
return oapi.openapi_schema
oapi = FastAPI()
oapi.openapi = custom_openapi
def apiroute(route=None, **kw):
routing = kw.copy()
def apidecorator(f):
if route:
if isinstance(route, list):
routes = route
else:
routes = [route]
routing['routes'] = routes
@functools.wraps(f)
def response_wrap(*args, **kw):
response = f(*args, **kw)
return response
oapi.add_api_route(routes[0], f, include_in_schema=True)
response_wrap.routing = routing
response_wrap.original_func = f
return response_wrap
return apidecorator
#

2
galicea_openapi/security/ir.model.access.csv

@ -1,2 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_galicea_openapi,galicea_openapi,model_galicea_openapi_openapi_script,galicea_openapi.script,1,1,1,0

BIN
galicea_openapi/static/description/icon.png

Before

Width: 128  |  Height: 128  |  Size: 10 KiB

1
galicea_openid_connect/__init__.py

@ -2,6 +2,5 @@
from . import controllers from . import controllers
from . import models from . import models
from . import system_checks
from . import api from . import api

71
galicea_openid_connect/__manifest__.py

@ -1,51 +1,38 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
{ {
'name': "Galicea OpenID Connect Provider",
'summary': """OpenID Connect Provider and OAuth2 resource server""",
'author': "Maciej Wawro",
'maintainer': "Galicea",
'website': "http://galicea.pl",
'category': 'Technical Settings',
'version': '12.0.0.0',
'depends': ['web', 'galicea_environment_checkup', 'galicea_base' ],
'external_dependencies': {
'python': ['jwcrypto', 'cryptography']
},
'data': [
'security/security.xml',
'security/ir.model.access.csv',
# 'security/init.yml',
'security/init.xml',
'views/views.xml',
'views/templates.xml'
"name": "Galicea OpenID Connect Provider",
"summary": """OpenID Connect Provider and OAuth2 resource server""",
"author": "Maciej Wawro, Nicolas JEUDY",
"maintainer": "nj.0k.io, Galicea",
"website": "https://nj.0k.io",
"category": "Technical Settings",
"version": "16.0.0.0",
"depends": ["web"],
"external_dependencies": {"python": ["jwcrypto", "cryptography"]},
"data": [
"security/security.xml",
"security/ir.model.access.csv",
# 'security/init.yml',
"security/init.xml",
"views/views.xml",
"views/templates.xml",
], ],
'environment_checkup': {
'dependencies': {
'python': [
"environment_checkup": {
"dependencies": {
"python": [
{"name": "jwcrypto", "install": "pip install 'jwcrypto==0.5.0'"},
{ {
'name': 'jwcrypto',
'install': "pip install 'jwcrypto==0.5.0'"
"name": "cryptography",
"version": ">=2.3",
"install": "pip install 'cryptography>=2.3'",
}, },
{
'name': 'cryptography',
'version': '>=2.3',
'install': "pip install 'cryptography>=2.3'"
}
] ]
} }
}, },
'images': [
'static/description/images/master_screenshot.png',
'static/description/images/client_screenshot.png',
'static/description/images/login_screenshot.png',
'static/description/images/error_screenshot.png'
]
"images": [
"static/description/images/master_screenshot.png",
"static/description/images/client_screenshot.png",
"static/description/images/login_screenshot.png",
"static/description/images/error_screenshot.png",
],
} }

23
galicea_openid_connect/api.py

@ -5,6 +5,7 @@ import logging
from functools import wraps from functools import wraps
from odoo import http from odoo import http
from odoo.http import request
import werkzeug import werkzeug
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -22,13 +23,13 @@ def resource(path, method, auth='user'):
def endpoint_decorator(func): def endpoint_decorator(func):
@http.route(path, auth='public', type='http', methods=[method, 'OPTIONS'], csrf=False) @http.route(path, auth='public', type='http', methods=[method, 'OPTIONS'], csrf=False)
@wraps(func) @wraps(func)
def func_wrapper(self, req, **query):
def func_wrapper(self, **query):
cors_headers = { cors_headers = {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-Debug-Mode, Authorization', 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-Debug-Mode, Authorization',
'Access-Control-Max-Age': 60 * 60 * 24, 'Access-Control-Max-Age': 60 * 60 * 24,
} }
if req.httprequest.method == 'OPTIONS':
if request.httprequest.method == 'OPTIONS':
return http.Response( return http.Response(
status=200, status=200,
headers=cors_headers headers=cors_headers
@ -36,8 +37,8 @@ def resource(path, method, auth='user'):
try: try:
access_token = None access_token = None
if 'Authorization' in req.httprequest.headers:
authorization_header = req.httprequest.headers['Authorization']
if 'Authorization' in request.httprequest.headers:
authorization_header = request.httprequest.headers['Authorization']
if authorization_header[:7] == 'Bearer ': if authorization_header[:7] == 'Bearer ':
access_token = authorization_header.split(' ', 1)[1] access_token = authorization_header.split(' ', 1)[1]
if access_token is None: if access_token is None:
@ -48,7 +49,7 @@ def resource(path, method, auth='user'):
'invalid_request', 'invalid_request',
) )
if auth == 'user': if auth == 'user':
token = req.env['galicea_openid_connect.access_token'].sudo().search(
token = request.env['galicea_openid_connect.access_token'].sudo().search(
[('token', '=', access_token)] [('token', '=', access_token)]
) )
if not token: if not token:
@ -56,9 +57,9 @@ def resource(path, method, auth='user'):
'access_token is invalid', 'access_token is invalid',
'invalid_request', 'invalid_request',
) )
req.uid = token.user_id.id
request.update_env(user=token.user_id.id)
elif auth == 'client': elif auth == 'client':
token = req.env['galicea_openid_connect.client_access_token'].sudo().search(
token = request.env['galicea_openid_connect.client_access_token'].sudo().search(
[('token', '=', access_token)] [('token', '=', access_token)]
) )
if not token: if not token:
@ -66,13 +67,11 @@ def resource(path, method, auth='user'):
'access_token is invalid', 'access_token is invalid',
'invalid_request', 'invalid_request',
) )
req.uid = token.client_id.system_user_id.id
request.update_env(user=token.client_id.system_user_id.id)
ctx = req.context.copy()
ctx.update({'client_id': token.client_id.id})
req.context = ctx
request.update_env(context={'client_id': token.client_id.id})
response = func(self, req, **query)
response = func(self, **query)
return werkzeug.Response( return werkzeug.Response(
response=json.dumps(response), response=json.dumps(response),
headers=cors_headers, headers=cors_headers,

7
galicea_openid_connect/controllers/ext_web_login.py

@ -3,15 +3,16 @@
import time import time
from odoo import http from odoo import http
from odoo.http import request
from odoo.addons import web from odoo.addons import web
class Home(web.controllers.main.Home):
class Home(web.controllers.home.Home):
@http.route('/web/login', type='http', auth="none") @http.route('/web/login', type='http', auth="none")
def web_login(self, redirect=None, **kw): def web_login(self, redirect=None, **kw):
result = super(Home, self).web_login(redirect, **kw) result = super(Home, self).web_login(redirect, **kw)
if result.is_qweb and 'force_auth_and_redirect' in kw: if result.is_qweb and 'force_auth_and_redirect' in kw:
result.qcontext['redirect'] = kw['force_auth_and_redirect'] result.qcontext['redirect'] = kw['force_auth_and_redirect']
if http.request.params.get('login_success'):
http.request.session['auth_time'] = int(time.time())
if request.params.get('login_success'):
request.session['auth_time'] = int(time.time())
return result return result

111
galicea_openid_connect/controllers/main.py

@ -1,12 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
import logging
import time import time
import os import os
import base64 import base64
from odoo import http from odoo import http
from odoo.http import request
import werkzeug import werkzeug
from werkzeug.urls import url_encode
from .. api import resource from .. api import resource
@ -17,6 +20,8 @@ try:
except ImportError: except ImportError:
pass pass
_logger = logging.getLogger(__name__)
def jwk_from_json(json_key): def jwk_from_json(json_key):
key = jwk.JWK() key = jwk.JWK()
key.import_key(**json.loads(json_key)) key.import_key(**json.loads(json_key))
@ -53,24 +58,24 @@ class OAuthException(Exception):
self.type = type self.type = type
class Main(http.Controller): class Main(http.Controller):
def __get_authorization_code_jwk(self, req):
return jwk_from_json(req.env['ir.config_parameter'].sudo().get_param(
def __get_authorization_code_jwk(self):
return jwk_from_json(request.env['ir.config_parameter'].sudo().get_param(
'galicea_openid_connect.authorization_code_jwk' 'galicea_openid_connect.authorization_code_jwk'
)) ))
def __get_id_token_jwk(self, req):
return jwk_from_json(req.env['ir.config_parameter'].sudo().get_param(
def __get_id_token_jwk(self):
return jwk_from_json(request.env['ir.config_parameter'].sudo().get_param(
'galicea_openid_connect.id_token_jwk' 'galicea_openid_connect.id_token_jwk'
)) ))
def __validate_client(self, req, **query):
def __validate_client(self, **query):
if 'client_id' not in query: if 'client_id' not in query:
raise OAuthException( raise OAuthException(
'client_id param is missing', 'client_id param is missing',
OAuthException.INVALID_CLIENT, OAuthException.INVALID_CLIENT,
) )
client_id = query['client_id'] client_id = query['client_id']
client = req.env['galicea_openid_connect.client'].sudo().search(
client = request.env['galicea_openid_connect.client'].sudo().search(
[('client_id', '=', client_id)] [('client_id', '=', client_id)]
) )
if not client: if not client:
@ -80,7 +85,7 @@ class Main(http.Controller):
) )
return client return client
def __validate_redirect_uri(self, client, req, **query):
def __validate_redirect_uri(self, client, **query):
if 'redirect_uri' not in query: if 'redirect_uri' not in query:
raise OAuthException( raise OAuthException(
'redirect_uri param is missing', 'redirect_uri param is missing',
@ -96,7 +101,7 @@ class Main(http.Controller):
return redirect_uri return redirect_uri
def __validate_client_secret(self, client, req, **query):
def __validate_client_secret(self, client, **query):
if 'client_secret' not in query or query['client_secret'] != client.secret: if 'client_secret' not in query or query['client_secret'] != client.secret:
raise OAuthException( raise OAuthException(
'client_secret param is not valid', 'client_secret param is not valid',
@ -104,7 +109,7 @@ class Main(http.Controller):
) )
@http.route('/.well-known/openid-configuration', auth='public', type='http') @http.route('/.well-known/openid-configuration', auth='public', type='http')
def metadata(self, req, **query):
def metadata(self, **query):
base_url = http.request.httprequest.host_url base_url = http.request.httprequest.host_url
data = { data = {
'issuer': base_url, 'issuer': base_url,
@ -122,14 +127,14 @@ class Main(http.Controller):
return json.dumps(data) return json.dumps(data)
@http.route('/oauth/jwks', auth='public', type='http') @http.route('/oauth/jwks', auth='public', type='http')
def jwks(self, req, **query):
def jwks(self, **query):
keyset = jwk.JWKSet() keyset = jwk.JWKSet()
keyset.add(self.__get_id_token_jwk(req))
keyset.add(self.__get_id_token_jwk())
return keyset.export(private_keys=False) return keyset.export(private_keys=False)
@resource('/oauth/userinfo', method='GET') @resource('/oauth/userinfo', method='GET')
def userinfo(self, req, **query):
user = req.env.user
def userinfo(self, **query):
user = request.env.user
values = { values = {
'sub': str(user.id), 'sub': str(user.id),
# Needed in case the client is another Odoo instance # Needed in case the client is another Odoo instance
@ -141,22 +146,22 @@ class Main(http.Controller):
return values return values
@resource('/oauth/clientinfo', method='GET', auth='client') @resource('/oauth/clientinfo', method='GET', auth='client')
def clientinfo(self, req, **query):
client = req.env['galicea_openid_connect.client'].browse(req.context['client_id'])
def clientinfo(self, **query):
client = request.env['galicea_openid_connect.client'].browse(request.context['client_id'])
return { return {
'name': client.name 'name': client.name
} }
@http.route('/oauth/authorize', auth='public', type='http', csrf=False) @http.route('/oauth/authorize', auth='public', type='http', csrf=False)
def authorize(self, req, **query):
def authorize(self, **query):
# First, validate client_id and redirect_uri params. # First, validate client_id and redirect_uri params.
try: try:
client = self.__validate_client(req, **query)
redirect_uri = self.__validate_redirect_uri(client, req, **query)
client = self.__validate_client(**query)
redirect_uri = self.__validate_redirect_uri(client, **query)
except OAuthException as e: except OAuthException as e:
# If those are not valid, we must not redirect back to the client # If those are not valid, we must not redirect back to the client
# - instead, we display a message to the user # - instead, we display a message to the user
return req.render('galicea_openid_connect.error', {'exception': e})
return request.render('galicea_openid_connect.error', {'exception': e})
scopes = query['scope'].split(' ') if query.get('scope') else [] scopes = query['scope'].split(' ') if query.get('scope') else []
is_openid_request = 'openid' in scopes is_openid_request = 'openid' in scopes
@ -194,7 +199,7 @@ class Main(http.Controller):
if not response_mode: if not response_mode:
response_mode = 'query' if response_type == 'code' else 'fragment' response_mode = 'query' if response_type == 'code' else 'fragment'
user = req.env.user
user = request.env.user
# In case user is not logged in, we redirect to the login page and come back # In case user is not logged in, we redirect to the login page and come back
needs_login = user.login == 'public' needs_login = user.login == 'public'
# Also if they didn't authenticate recently enough # Also if they didn't authenticate recently enough
@ -202,14 +207,14 @@ class Main(http.Controller):
needs_login = True needs_login = True
if needs_login: if needs_login:
params = { params = {
'force_auth_and_redirect': '/oauth/authorize?{}'.format(werkzeug.url_encode(query))
'force_auth_and_redirect': '/oauth/authorize?{}'.format(url_encode(query))
} }
return self.__redirect('/web/login', params, 'query') return self.__redirect('/web/login', params, 'query')
response_types = response_type.split() response_types = response_type.split()
extra_claims = { extra_claims = {
'sid': http.request.httprequest.session.sid,
'sid': http.request.session.sid,
} }
if 'nonce' in query: if 'nonce' in query:
extra_claims['nonce'] = query['nonce'] extra_claims['nonce'] = query['nonce']
@ -227,10 +232,10 @@ class Main(http.Controller):
'exp': int(time.time()) + 60 'exp': int(time.time()) + 60
} }
payload.update(extra_claims) payload.update(extra_claims)
key = self.__get_authorization_code_jwk(req)
key = self.__get_authorization_code_jwk()
response_params['code'] = jwt_encode(payload, key) response_params['code'] = jwt_encode(payload, key)
if 'token' in response_types: if 'token' in response_types:
access_token = req.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create(
access_token = request.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create(
user.id, user.id,
client.id client.id
).token ).token
@ -243,23 +248,24 @@ class Main(http.Controller):
#extra_claims['at_hash'] = base64.urlsafe_b64encode(at_hash[:16]).strip('=') #extra_claims['at_hash'] = base64.urlsafe_b64encode(at_hash[:16]).strip('=')
extra_claims['at_hash'] = base64.urlsafe_b64encode(at_hash[:16]) extra_claims['at_hash'] = base64.urlsafe_b64encode(at_hash[:16])
if 'id_token' in response_types: if 'id_token' in response_types:
response_params['id_token'] = self.__create_id_token(req, user.id, client, extra_claims)
response_params['id_token'] = self.__create_id_token(user.id, client, extra_claims)
return self.__redirect(redirect_uri, response_params, response_mode) return self.__redirect(redirect_uri, response_params, response_mode)
@http.route('/oauth/token', auth='public', type='http', methods=['POST', 'OPTIONS'], csrf=False) @http.route('/oauth/token', auth='public', type='http', methods=['POST', 'OPTIONS'], csrf=False)
def token(self, req, **query):
def token(self, **query):
cors_headers = { cors_headers = {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-Debug-Mode, Authorization', 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-Debug-Mode, Authorization',
'Access-Control-Max-Age': 60 * 60 * 24, 'Access-Control-Max-Age': 60 * 60 * 24,
} }
if req.httprequest.method == 'OPTIONS':
_logger.info("Test1 %s" % request.httprequest.method)
if request.httprequest.method == 'OPTIONS':
return http.Response( return http.Response(
status=200, status=200,
headers=cors_headers headers=cors_headers
) )
_logger.info("Test2 %s" % query)
try: try:
if 'grant_type' not in query: if 'grant_type' not in query:
raise OAuthException( raise OAuthException(
@ -267,12 +273,15 @@ class Main(http.Controller):
OAuthException.INVALID_REQUEST, OAuthException.INVALID_REQUEST,
) )
if query['grant_type'] == 'authorization_code': if query['grant_type'] == 'authorization_code':
return json.dumps(self.__handle_grant_type_authorization_code(req, **query))
_logger.info("Test3")
return json.dumps(self.__handle_grant_type_authorization_code(**query))
elif query['grant_type'] == 'client_credentials': elif query['grant_type'] == 'client_credentials':
return json.dumps(self.__handle_grant_type_client_credentials(req, **query))
_logger.info("Test4")
return json.dumps(self.__handle_grant_type_client_credentials(**query))
elif query['grant_type'] == 'password': elif query['grant_type'] == 'password':
_logger.info("Test5")
return werkzeug.Response( return werkzeug.Response(
response=json.dumps(self.__handle_grant_type_password(req, **query)),
response=json.dumps(self.__handle_grant_type_password(**query)),
headers=cors_headers headers=cors_headers
) )
else: else:
@ -284,10 +293,10 @@ class Main(http.Controller):
body = json.dumps({'error': e.type, 'error_description': e}) body = json.dumps({'error': e.type, 'error_description': e})
return werkzeug.Response(response=body, status=400, headers=cors_headers) return werkzeug.Response(response=body, status=400, headers=cors_headers)
def __handle_grant_type_authorization_code(self, req, **query):
client = self.__validate_client(req, **query)
redirect_uri = self.__validate_redirect_uri(client, req, **query)
self.__validate_client_secret(client, req, **query)
def __handle_grant_type_authorization_code(self, **query):
client = self.__validate_client(**query)
redirect_uri = self.__validate_redirect_uri(client, **query)
self.__validate_client_secret(client, **query)
if 'code' not in query: if 'code' not in query:
raise OAuthException( raise OAuthException(
@ -295,7 +304,7 @@ class Main(http.Controller):
OAuthException.INVALID_GRANT, OAuthException.INVALID_GRANT,
) )
try: try:
payload = jwt_decode(query['code'], self.__get_authorization_code_jwk(req))
payload = jwt_decode(query['code'], self.__get_authorization_code_jwk())
except jwt.JWTExpired: except jwt.JWTExpired:
raise OAuthException( raise OAuthException(
'Code expired', 'Code expired',
@ -318,7 +327,7 @@ class Main(http.Controller):
) )
# Retrieve/generate access token. We currently only store one per user/client # Retrieve/generate access token. We currently only store one per user/client
token = req.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create(
token = request.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create(
payload['user_id'], payload['user_id'],
client.id client.id
) )
@ -328,11 +337,11 @@ class Main(http.Controller):
} }
if 'openid' in payload['scopes']: if 'openid' in payload['scopes']:
extra_claims = { name: payload[name] for name in payload if name in ['sid', 'nonce'] } extra_claims = { name: payload[name] for name in payload if name in ['sid', 'nonce'] }
response['id_token'] = self.__create_id_token(req, payload['user_id'], client, extra_claims)
response['id_token'] = self.__create_id_token(payload['user_id'], client, extra_claims)
return response return response
def __handle_grant_type_password(self, req, **query):
client = self.__validate_client(req, **query)
def __handle_grant_type_password(self, **query):
client = self.__validate_client(**query)
if not client.allow_password_grant: if not client.allow_password_grant:
raise OAuthException( raise OAuthException(
'This client is not allowed to perform password flow', 'This client is not allowed to perform password flow',
@ -345,8 +354,8 @@ class Main(http.Controller):
'{} is required'.format(param), '{} is required'.format(param),
OAuthException.INVALID_REQUEST OAuthException.INVALID_REQUEST
) )
user_id = req.env['res.users'].authenticate(
req.env.cr.dbname,
user_id = request.env['res.users'].authenticate(
request.env.cr.dbname,
query['username'], query['username'],
query['password'], query['password'],
None None
@ -359,7 +368,7 @@ class Main(http.Controller):
scopes = query['scope'].split(' ') if query.get('scope') else [] scopes = query['scope'].split(' ') if query.get('scope') else []
# Retrieve/generate access token. We currently only store one per user/client # Retrieve/generate access token. We currently only store one per user/client
token = req.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create(
token = request.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create(
user_id, user_id,
client.id client.id
) )
@ -368,19 +377,19 @@ class Main(http.Controller):
'token_type': 'bearer' 'token_type': 'bearer'
} }
if 'openid' in scopes: if 'openid' in scopes:
response['id_token'] = self.__create_id_token(req, user_id, client, {})
response['id_token'] = self.__create_id_token(user_id, client, {})
return response return response
def __handle_grant_type_client_credentials(self, req, **query):
client = self.__validate_client(req, **query)
self.__validate_client_secret(client, req, **query)
token = req.env['galicea_openid_connect.client_access_token'].sudo().retrieve_or_create(client.id)
def __handle_grant_type_client_credentials(self, **query):
client = self.__validate_client(**query)
self.__validate_client_secret(client, **query)
token = request.env['galicea_openid_connect.client_access_token'].sudo().retrieve_or_create(client.id)
return { return {
'access_token': token.token, 'access_token': token.token,
'token_type': 'bearer' 'token_type': 'bearer'
} }
def __create_id_token(self, req, user_id, client, extra_claims):
def __create_id_token(self, user_id, client, extra_claims):
claims = { claims = {
'iss': http.request.httprequest.host_url, 'iss': http.request.httprequest.host_url,
'sub': str(user_id), 'sub': str(user_id),
@ -396,14 +405,14 @@ class Main(http.Controller):
if 'at_hash' in extra_claims: if 'at_hash' in extra_claims:
claims['at_hash'] = extra_claims['at_hash'] claims['at_hash'] = extra_claims['at_hash']
key = self.__get_id_token_jwk(req)
key = self.__get_id_token_jwk()
return jwt_encode(claims, key) return jwt_encode(claims, key)
def __redirect(self, url, params, response_mode): def __redirect(self, url, params, response_mode):
location = '{}{}{}'.format( location = '{}{}{}'.format(
url, url,
'?' if response_mode == 'query' else '#', '?' if response_mode == 'query' else '#',
werkzeug.url_encode(params)
url_encode(params)
) )
return werkzeug.Response( return werkzeug.Response(
headers={'Location': location}, headers={'Location': location},

57
galicea_openid_connect/models/client.py

@ -3,65 +3,68 @@
from odoo import models, fields, api from odoo import models, fields, api
from .. import random_tokens from .. import random_tokens
class Client(models.Model): class Client(models.Model):
_name = 'galicea_openid_connect.client'
_description = 'OpenID Connect client'
_name = "galicea_openid_connect.client"
_description = "OpenID Connect client"
name = fields.Char(required=True) name = fields.Char(required=True)
auth_redirect_uri = fields.Char('Redirect URI for user login')
auth_redirect_uri = fields.Char("Redirect URI for user login")
client_id = fields.Char( client_id = fields.Char(
string='Client ID',
string="Client ID",
required=True, required=True,
readonly=True,
readonly=False,
index=True, index=True,
default=lambda _: random_tokens.lower_case(16), default=lambda _: random_tokens.lower_case(16),
) )
secret = fields.Char( secret = fields.Char(
string='Client secret',
string="Client secret",
required=True, required=True,
readonly=True,
readonly=False,
default=lambda _: random_tokens.alpha_numeric(32), default=lambda _: random_tokens.alpha_numeric(32),
groups='galicea_openid_connect.group_admin'
groups="galicea_openid_connect.group_admin",
) )
system_user_id = fields.Many2one( system_user_id = fields.Many2one(
'res.users',
'Artificial user representing the client in client credentials requests',
"res.users",
"Artificial user representing the client in client credentials requests",
readonly=True, readonly=True,
required=True, required=True,
ondelete='restrict'
ondelete="restrict",
) )
allow_password_grant = fields.Boolean( allow_password_grant = fields.Boolean(
string='Allow OAuth2 password grant',
string="Allow OAuth2 password grant",
default=False, default=False,
) )
@api.model @api.model
def __system_user_name(self, client_name): def __system_user_name(self, client_name):
return '{} - API system user'.format(client_name)
return "{} - API system user".format(client_name)
@api.model @api.model
def create(self, values): def create(self, values):
if 'name' in values:
system_user = self.env['res.users'].create({
'name': self.__system_user_name(values['name']),
'login': random_tokens.lower_case(8),
'groups_id': [(4, self.env.ref('galicea_openid_connect.group_system_user').id)]
})
if "name" in values:
system_user = self.env["res.users"].create(
{
"name": self.__system_user_name(values["name"]),
"login": random_tokens.lower_case(8),
"groups_id": [
(4, self.env.ref("galicea_openid_connect.group_system_user").id)
],
}
)
# Do not include in the "Pending invitations" list # Do not include in the "Pending invitations" list
system_user.sudo(system_user.id)._update_last_login()
values['system_user_id'] = system_user.id
system_user._update_last_login()
values["system_user_id"] = system_user.id
return super(Client, self).create(values) return super(Client, self).create(values)
@api.multi
def write(selfs, values): def write(selfs, values):
super(Client, selfs).write(values) super(Client, selfs).write(values)
if 'name' in values:
selfs.mapped(lambda client: client.system_user_id).write({
'name': selfs.__system_user_name(values['name'])
})
if "name" in values:
selfs.mapped(lambda client: client.system_user_id).write(
{"name": selfs.__system_user_name(values["name"])}
)
return True return True
@api.multi
def unlink(selfs): def unlink(selfs):
users_to_unlink = selfs.mapped(lambda client: client.system_user_id) users_to_unlink = selfs.mapped(lambda client: client.system_user_id)
ret = super(Client, selfs).unlink() ret = super(Client, selfs).unlink()

45
galicea_openid_connect/models/config_parameter.py

@ -2,27 +2,40 @@
from odoo import models, fields, api from odoo import models, fields, api
from .. import random_tokens from .. import random_tokens
try: try:
from jwcrypto import jwk from jwcrypto import jwk
except ImportError: except ImportError:
pass pass
class ConfigParameter(models.Model): class ConfigParameter(models.Model):
_inherit = 'ir.config_parameter'
_inherit = "ir.config_parameter"
@api.model
def openid_init_keys(self):
keys = {
'galicea_openid_connect.authorization_code_jwk': lambda: \
jwk.JWK.generate(kty='oct', size=256, kid=random_tokens.alpha_numeric(16), use='sig', alg='HS256').export(),
'galicea_openid_connect.id_token_jwk': lambda: \
jwk.JWK.generate(kty='RSA', size=2054, kid=random_tokens.alpha_numeric(16), use='sig', alg='RS256').export()
}
@api.model
def openid_init_keys(self):
keys = {
"galicea_openid_connect.authorization_code_jwk": lambda: jwk.JWK.generate(
kty="oct",
size=256,
kid=random_tokens.alpha_numeric(16),
use="sig",
alg="HS256",
).export(),
"galicea_openid_connect.id_token_jwk": lambda: jwk.JWK.generate(
kty="RSA",
size=2054,
kid=random_tokens.alpha_numeric(16),
use="sig",
alg="RS256",
).export(),
}
for key, gen in iter(keys.items()):
if not self.search([('key', '=', key)]):
self.create({
'key': key,
'value': gen(),
'group_ids': [(4, self.env.ref('base.group_erp_manager').id)]
})
for key, gen in iter(keys.items()):
if not self.search([("key", "=", key)]):
self.create(
{
"key": key,
"value": gen(),
}
)

33
galicea_openid_connect/security/__init__.py

@ -1,23 +1,36 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from .. import random_tokens from .. import random_tokens
try: try:
from jwcrypto import jwk from jwcrypto import jwk
except ImportError: except ImportError:
pass pass
def init_keys(IrConfigParameter): def init_keys(IrConfigParameter):
keys = { keys = {
'galicea_openid_connect.authorization_code_jwk': lambda: \
jwk.JWK.generate(kty='oct', size=256, kid=random_tokens.alpha_numeric(16), use='sig', alg='HS256').export(),
'galicea_openid_connect.id_token_jwk': lambda: \
jwk.JWK.generate(kty='RSA', size=2054, kid=random_tokens.alpha_numeric(16), use='sig', alg='RS256').export()
"galicea_openid_connect.authorization_code_jwk": lambda: jwk.JWK.generate(
kty="oct",
size=256,
kid=random_tokens.alpha_numeric(16),
use="sig",
alg="HS256",
).export(),
"galicea_openid_connect.id_token_jwk": lambda: jwk.JWK.generate(
kty="RSA",
size=2054,
kid=random_tokens.alpha_numeric(16),
use="sig",
alg="RS256",
).export(),
} }
for key, gen in keys.iteritems(): for key, gen in keys.iteritems():
if not IrConfigParameter.search([('key', '=', key)]):
IrConfigParameter.create({
'key': key,
'value': gen(),
'group_ids': [(4, IrConfigParameter.env.ref('base.group_erp_manager').id)]
})
if not IrConfigParameter.search([("key", "=", key)]):
IrConfigParameter.create(
{
"key": key,
"value": gen(),
}
)

24
galicea_openid_connect/system_checks.py

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
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 API request routing 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
)

22
galicea_openid_connect/views/views.xml

@ -21,11 +21,11 @@
<form> <form>
<group> <group>
<field name="name" /> <field name="name" />
<field name="create_date" invisible="1" />
<field name="client_id"
<field name="create_date" invisible="1"/>
<field name="client_id" readonly="1"
attrs="{'invisible':[('create_date', '==', False)]}" /> attrs="{'invisible':[('create_date', '==', False)]}" />
<label for="secret" class="oe_read_only" string="Client Secret" />
<button class="oe_read_only" string="Show" type="action" name="%(client_action_secret)d" />
<label for="secret" string="Client Secret" />
<field name="secret" readonly="1" nolabel="1" />
<field name="auth_redirect_uri" /> <field name="auth_redirect_uri" />
<field name="allow_password_grant" /> <field name="allow_password_grant" />
</group> </group>
@ -33,18 +33,6 @@
</field> </field>
</record> </record>
<record id="client_view_form_secret" model="ir.ui.view">
<field name="inherit_id" ref="galicea_openid_connect.client_view_form" />
<field name="priority">99</field>
<field name="model">galicea_openid_connect.client</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<button name="%(client_action_secret)d" position="replace">
<field class="oe_read_only" name="secret" nolabel="1" />
</button>
</field>
</record>
<record id="client_view_tree" model="ir.ui.view"> <record id="client_view_tree" model="ir.ui.view">
<field name="model">galicea_openid_connect.client</field> <field name="model">galicea_openid_connect.client</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
@ -61,6 +49,6 @@
res_model="galicea_openid_connect.client" /> res_model="galicea_openid_connect.client" />
<menuitem name="OpenID Connect Provider" id="client_menu" <menuitem name="OpenID Connect Provider" id="client_menu"
parent="galicea_base.galicea_admin_menu" action="client_action" />
parent="base.menu_users" action="client_action" />
</data> </data>
</odoo> </odoo>

30
galicea_toolset/README.md

@ -1,30 +0,0 @@
Widgets
=======
<field name="url_field" widget="iframe" style="width: 100%" iframe_style="width: 100%" />
Creates an iframe with ``url_field`` value as a source.
<field name="article_ids" widget="one2many_flexible" click_target="current" />
Allows changing the target for the item click action.
Functions
=========
odoo.addons.galicea_toolset.utils.get_base_url(env)
Client actions
==============
@api.multi
def button_action(self):
return {
'type': 'ir.actions.client',
'tag': 'galicea_toolset.open_edit_dialog',
'params': { 'res_id': <id>, 'res_model': <model>, 'title': <title>}
};

1
galicea_toolset/__init__.py

@ -1 +0,0 @@
# -*- coding: utf-8 -*-

20
galicea_toolset/__manifest__.py

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': "galicea toolset",
'summary': """
A couple of small convenience widgets and functions""",
'author': "Maciej Wawro",
'maintainer': "Galicea",
'website': "http://www.galicea.pl",
'category': 'Technical Settings',
'version': '12.0.0.1',
'depends': ['base'],
'data': [
'views/data.xml'
],
}

1
galicea_toolset/static/src/js/.#one2many_flexible_widget.js

@ -1 +0,0 @@
jurek@jurek.15022

38
galicea_toolset/static/src/js/client_actions.js

@ -1,38 +0,0 @@
odoo.define('galicea_toolset.client_actions', function(require) {
var Widget = require('web.Widget');
var core = require('web.core');
var common = require('web.form_common');
var ActionManager = require('web.ActionManager');
var OpenEditDialogAction = Widget.extend({
init: function(parent, context) {
this._super.apply(this, arguments);
this.context = context;
if (parent instanceof ActionManager) {
this.am = parent;
}
},
start: function () {
var params = this.context.params;
var popup = new common.FormViewDialog(self, {
title: params.title,
res_model: params.res_model,
res_id: params.res_id,
}).open();
popup.on('closed', this, function() {
this.am && this.am.history_back();
});
},
});
core.action_registry.add(
'galicea_toolset.open_edit_dialog',
OpenEditDialogAction
);
return {
open_edit_dialog_action: OpenEditDialogAction,
};
});

49
galicea_toolset/static/src/js/iframe_widget.js

@ -1,49 +0,0 @@
odoo.define('pwste_epub.iframe_widget', function(require) {
var AbstractField = require('web.AbstractField');
var fieldRegistry = require('web.field_registry');
var core = require('web.core');
var Widget= require('web.Widget');
var widgetRegistry = require('web.widget_registry');
var FieldManagerMixin = require('web.FieldManagerMixin');
var IFrameWidget = AbstractField.extend({
init: function () {
this._super.apply(this, arguments);
// this.set("value", "");
},
_renderReadonly: function() {
window.widget=this;
this.$el.html(
$('<iframe>', {
src: this.value || 'about:blank',
style: this.attrs.iframe_style
})
);
if (this.attrs.new_window_label && this.value) {
this.$el.prepend(
$('<a>', {
href: this.value,
target: '_blank',
style: 'float:right; margin-bottom: 10px',
'class': 'btn btn-primary',
}).html('<i class="fa fa-external-link-square" aria-hidden="true"></i> Otwórz w nowym oknie')
)
}
},
});
fieldRegistry.add(
'iframe', IFrameWidget
);
return {
IFrameWidget: IFrameWidget,
};
});

68
galicea_toolset/static/src/js/one2many_flexible_widget.js

@ -1,68 +0,0 @@
odoo.define('galicea_toolset.one2many_flexible_widget', function(require) {
var core = require('web.core');
/*
var view_dialogs = require('web.view_dialogs'),
relational_fields = require('web.relational_fields'),
rpc = require('web.rpc'),
field_registry = require('web.field_registry');*/
var form_relational = require('web.form_relational');
/* var X2ManyList = form_relational.X2ManyList;
var ListView = require('web.ListView');
var FieldOne2Many = field_registry.get('one2many');
var FieldOne2Many = relational_fields.FieldOne2Many;
var FormController = require('web.FormController');
*/
/*
ListView.include({
do_activate_record: function (index, id, dataset, view) {
var action = this.ViewManager.action;
if (!action || !action.context || !action.context.open_formview)
return this._super(index, id, dataset, view);
do_action(this, id, action.context);
}
});
var One2ManyListView = core.one2many_view_registry.get('list');
*/
var One2ManyFlexibleListView = form_relational.One2ManyListView.extend({
do_activate_record: function(index, id) {
var self = this;
if (!this.x2m.get("effective_readonly")) {
this._super.apply(this, arguments);
return;
}
this.do_action({
'type': 'ir.actions.act_window',
'views': [[false, 'form']],
'res_model': self.x2m.field.relation,
'res_id': id,
'target': self.x2m.node.attrs.click_target || 'current',
});
}
});
var FieldOne2Many = core.form_widget_registry.get('one2many');
var FieldOne2ManyFlexible = FieldOne2Many.extend({
init: function() {
this._super.apply(this, arguments);
this.x2many_views = {
kanban: core.view_registry.get('one2many_kanban'),
list: One2ManyFlexibleListView,
};
},
});
core.form_widget_registry.add('one2many_flexible', FieldOne2ManyFlexible);
return {
FieldOne2ManyFlexible: FieldOne2ManyFlexible,
};
});

16
galicea_toolset/utils.py

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import http
def get_base_url(env):
"""
Better host name detection
@param env odoo.api.Environment"""
if http.request:
# Preferuj nazwę hosta, która została użyta do tego zapytania
return http.request.httprequest.host_url
else:
# Jeśli nie jesteśmy wewnątrz zapytania HTTP, zwróć domenę ostatnio użytą
# przez admina do zalogowania
return env['ir.config_parameter'].get_param('web.base.url') + '/'

11
galicea_toolset/views/data.xml

@ -1,11 +0,0 @@
<odoo>
<data>
<template id="assets_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script src="/galicea_toolset/static/src/js/iframe_widget.js" type="text/javascript" />
<script src="/galicea_toolset/static/src/js/one2many_flexible_widget.js" type="text/javascript" />
<script src="/galicea_toolset/static/src/js/client_actions.js" type="text/javascript" />
</xpath>
</template>
</data>
</odoo>
Loading…
Cancel
Save