Merge branch 'flectra-license-server' into 'master'
[ADD] support for delayed release See merge request flectra-hq/flectra!163
This commit is contained in:
commit
35046ba1b9
@ -2,8 +2,10 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import api, fields, models, _
|
||||
import json
|
||||
|
||||
import json, base64, datetime, logging
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.addons.web.models.crypt import *
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
|
||||
@ -37,6 +39,8 @@ class ResConfigSettings(models.TransientModel):
|
||||
external_report_layout = fields.Selection(related="company_id.external_report_layout")
|
||||
send_statistics = fields.Boolean(
|
||||
"Send Statistics")
|
||||
activator_key = fields.Binary('Upload Activation Key')
|
||||
contract_id = fields.Char('Contract ID')
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
@ -72,6 +76,8 @@ class ResConfigSettings(models.TransientModel):
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
"base_setup.send_statistics", send_statistics)
|
||||
self.env.ref('base.res_partner_rule').write({'active': not self.company_share_partner})
|
||||
if self.activator_key:
|
||||
self._check_authorization()
|
||||
|
||||
@api.multi
|
||||
def open_company(self):
|
||||
@ -123,3 +129,22 @@ class ResConfigSettings(models.TransientModel):
|
||||
'view_id': template.id,
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def _check_authorization(self):
|
||||
if self.activator_key and self.contract_id:
|
||||
try:
|
||||
set_param = self.env['ir.config_parameter'].sudo().set_param
|
||||
binary = json.loads(base64.decodestring(self.activator_key)).encode('ascii')
|
||||
binary = base64.decodestring(binary)
|
||||
enc = json.dumps(decrypt(binary, self.contract_id))
|
||||
if enc:
|
||||
dt = datetime.datetime.strptime(json.loads(enc),'"%Y-%m-%d %H:%M:%S"')
|
||||
set_param('database.expiration_date', dt)
|
||||
set_param('contract.validity',
|
||||
base64.encodestring(
|
||||
encrypt(json.dumps(str(dt)),
|
||||
str(dt))))
|
||||
except Exception:
|
||||
_logger.info(_('Please double-check your Contract Key!'), exc_info=True)
|
||||
raise UserError(
|
||||
_('Authorization error!') + ' ' + _('Please double-check your Contract Key!'))
|
||||
|
@ -203,6 +203,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Contract Activation" for="activator_key"/>
|
||||
<div class="text-muted">
|
||||
Contract File
|
||||
<field name="activator_key"/>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
Contract ID
|
||||
<field name="contract_id"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>System Parameter</h2>
|
||||
<div class="row mt16 o_settings_container" id="send_statistics">
|
||||
|
@ -52,6 +52,7 @@ import requests
|
||||
from flectra.tools import config
|
||||
from flectra import release
|
||||
from flectra.http import root
|
||||
from ..models.crypt import *
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@ -69,6 +70,8 @@ env.filters["json"] = json.dumps
|
||||
BUNDLE_MAXAGE = 60 * 60 * 24 * 7
|
||||
|
||||
DBNAME_PATTERN = '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$'
|
||||
FILENAME = 'licence'
|
||||
EXT = 'key'
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Flectra Web helpers
|
||||
@ -1861,3 +1864,16 @@ class ReportController(http.Controller):
|
||||
@http.route(['/report/check_wkhtmltopdf'], type='json', auth="user")
|
||||
def check_wkhtmltopdf(self):
|
||||
return request.env['ir.actions.report'].get_wkhtmltopdf_state()
|
||||
|
||||
class LicensingController(http.Controller):
|
||||
@http.route('/flectra/licensing', type='http', auth="user")
|
||||
def download(self, binary='', **kwargs):
|
||||
filename = '%s.%s' % (FILENAME, EXT)
|
||||
content = binary
|
||||
return request.make_response(
|
||||
content,
|
||||
headers=[
|
||||
('Content-Type', 'plain/text' or 'application/octet-stream'),
|
||||
('Content-Disposition', content_disposition(filename))
|
||||
]
|
||||
)
|
||||
|
40
addons/web/models/crypt.py
Normal file
40
addons/web/models/crypt.py
Normal file
@ -0,0 +1,40 @@
|
||||
import Crypto.Random
|
||||
from Crypto.Cipher import AES
|
||||
import hashlib
|
||||
|
||||
# salt size in bytes
|
||||
SALT_SIZE = 16
|
||||
|
||||
# number of iterations in the key generation
|
||||
NUMBER_OF_ITERATIONS = 20
|
||||
|
||||
# the size multiple required for AES
|
||||
AES_MULTIPLE = 16
|
||||
|
||||
|
||||
def generate_key(password, salt, iterations):
|
||||
assert iterations > 0
|
||||
key = str.encode(password) + salt
|
||||
for i in range(iterations):
|
||||
key = hashlib.sha256(key).digest()
|
||||
return key
|
||||
|
||||
|
||||
def pad_text(text, multiple):
|
||||
return (text) + (chr((multiple - (len(text) % multiple))) * ((multiple - (len(text) % multiple))))
|
||||
|
||||
|
||||
def unpad_text(padded_text):
|
||||
return padded_text.decode('utf-8')[:-ord(padded_text.decode('utf-8')[-1])]
|
||||
|
||||
|
||||
def encrypt(plaintext, contract_id):
|
||||
salt = Crypto.Random.get_random_bytes(SALT_SIZE)
|
||||
return salt + (AES.new((generate_key(contract_id, salt, NUMBER_OF_ITERATIONS)), AES.MODE_ECB).encrypt(
|
||||
(pad_text(plaintext, AES_MULTIPLE))))
|
||||
|
||||
|
||||
def decrypt(ciphertext, contract_id):
|
||||
salt = ciphertext[0:SALT_SIZE]
|
||||
return unpad_text(
|
||||
AES.new((generate_key(contract_id, salt, NUMBER_OF_ITERATIONS)), AES.MODE_ECB).decrypt(ciphertext[SALT_SIZE:]))
|
@ -2,9 +2,11 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import base64
|
||||
|
||||
from flectra import models
|
||||
from flectra import models,api
|
||||
from flectra.http import request
|
||||
from .crypt import *
|
||||
|
||||
import flectra
|
||||
|
||||
@ -22,6 +24,16 @@ class Http(models.AbstractModel):
|
||||
user = request.env.user
|
||||
display_switch_company_menu = user.has_group('base.group_multi_company') and len(user.company_ids) > 1
|
||||
version_info = flectra.service.common.exp_version()
|
||||
ir_module_module_ids = self.env['ir.module.module'].sudo().search(
|
||||
[('contract_certificate', '!=', False), ('state', '=', 'installed')])
|
||||
IrConfig = request.env['ir.config_parameter'].sudo()
|
||||
contracted_module_list, is_valid = None, False
|
||||
if ir_module_module_ids:
|
||||
contracted_module_list = str(self.get_contracted_modules(ir_module_module_ids=ir_module_module_ids))
|
||||
is_valid = self.check_validate_date(IrConfig)
|
||||
else:
|
||||
is_valid = True
|
||||
|
||||
return {
|
||||
"session_id": request.session.sid,
|
||||
"uid": request.session.uid,
|
||||
@ -38,9 +50,35 @@ class Http(models.AbstractModel):
|
||||
"user_companies": {'current_company': (user.company_id.id, user.company_id.name), 'allowed_companies': [(comp.id, comp.name) for comp in user.company_ids]} if display_switch_company_menu else False,
|
||||
"currencies": self.get_currencies() if request.session.uid else {},
|
||||
"web.base.url": self.env['ir.config_parameter'].sudo().get_param('web.base.url', default=''),
|
||||
'expiration_date' : IrConfig.get_param('database.expiration_date'),
|
||||
'expiration_reason': IrConfig.get_param('database.expiration_reason'),
|
||||
'contracted_module_list': contracted_module_list,
|
||||
'contract_validation':is_valid
|
||||
}
|
||||
|
||||
def get_currencies(self):
|
||||
Currency = request.env['res.currency']
|
||||
currencies = Currency.search([]).read(['symbol', 'position', 'decimal_places'])
|
||||
return { c['id']: {'symbol': c['symbol'], 'position': c['position'], 'digits': [69,c['decimal_places']]} for c in currencies}
|
||||
|
||||
def get_contracted_modules(self, contract_key='', ir_module_module_ids=None):
|
||||
if ir_module_module_ids:
|
||||
contracted_module_list = ir_module_module_ids.mapped('name')
|
||||
contracts = encrypt(json.dumps(contracted_module_list), contract_key)
|
||||
return contracts
|
||||
|
||||
@api.model
|
||||
def contract_validate_file(self, contract_id):
|
||||
ir_module_module_ids = self.env['ir.module.module'].sudo().search(
|
||||
[('contract_certificate', '!=', False), ('state', '=', 'installed')])
|
||||
contracts = self.get_contracted_modules(contract_id,ir_module_module_ids)
|
||||
return json.dumps(base64.encodestring(contracts).decode('ascii'))
|
||||
|
||||
def check_validate_date(self, config):
|
||||
exp_date = config.get_param('database.expiration_date')
|
||||
validity = config.get_param('contract.validity')
|
||||
try:
|
||||
decrypt(base64.decodestring(str.encode(validity)), str(exp_date))
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
@ -0,0 +1,41 @@
|
||||
flectra.define('FlectraLicensing.DialogRegisterContract', function (require) {
|
||||
"use strict";
|
||||
var Dialog = require('web.Dialog');
|
||||
var rpc = require('web.rpc');
|
||||
|
||||
return Dialog.extend({
|
||||
template: 'FlectraLicense.dialog_contract_registration',
|
||||
init: function (parent) {
|
||||
var options = {
|
||||
title: 'Register Contract',
|
||||
size: 'small',
|
||||
buttons: [
|
||||
{
|
||||
text: "save",
|
||||
classes: 'btn-success',
|
||||
click: _.bind(this.save, this)
|
||||
},
|
||||
{text: "Cancel", classes: 'btn-danger', close: true}
|
||||
]
|
||||
};
|
||||
this._super(parent, options);
|
||||
},
|
||||
|
||||
save: function () {
|
||||
var contract_id = this.$el.find('#contract_id').val();
|
||||
var self = this;
|
||||
if (!contract_id) {
|
||||
return;
|
||||
}
|
||||
rpc.query({
|
||||
model: 'ir.http',
|
||||
method: 'contract_validate_file',
|
||||
args: [contract_id]
|
||||
}).done(function (bin) {
|
||||
self.trigger('get_key', {'key': contract_id, 'binary': bin});
|
||||
self.close();
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
});
|
@ -11,6 +11,9 @@ var SystrayMenu = require('web.SystrayMenu');
|
||||
var UserMenu = require('web.UserMenu');
|
||||
var UserProfile = require('web.UserProfile');
|
||||
var config = require('web.config');
|
||||
var rpc = require('web.rpc');
|
||||
var qweb = core.qweb;
|
||||
var Dialog = require('FlectraLicensing.DialogRegisterContract');
|
||||
|
||||
return AbstractWebClient.extend({
|
||||
events: {
|
||||
@ -55,6 +58,9 @@ return AbstractWebClient.extend({
|
||||
this.systray_menu.setElement(this.$el.parents().find('.oe_systray'));
|
||||
var systray_menu_loaded = this.systray_menu.start();
|
||||
|
||||
if ((session.expiration_date && session.expiration_reason === 'contract_expire') || !session['contract_validation']) {
|
||||
this.validate_days_of_contract();
|
||||
}
|
||||
// Start the menu once both systray and user menus are rendered
|
||||
// to prevent overflows while loading
|
||||
return $.when(systray_menu_loaded, user_menu_loaded).then(function() {
|
||||
@ -198,6 +204,95 @@ return AbstractWebClient.extend({
|
||||
this.menu.reflow();
|
||||
}
|
||||
},
|
||||
validate_days_of_contract: function () {
|
||||
var today = new moment();
|
||||
var dbexpiration_date = new moment(session.expiration_date);
|
||||
var duration = moment.duration(dbexpiration_date.diff(today));
|
||||
var params = {
|
||||
'difference': Math.round(duration.asDays()),
|
||||
'reason': session.expiration_reason,
|
||||
};
|
||||
this.show_contract_registration(params);
|
||||
},
|
||||
|
||||
show_contract_registration: function (params) {
|
||||
var self = this;
|
||||
var bg_color = params.difference <= 10 ? '#e55e50' : '#f3be5d';
|
||||
var difference = params.difference || 0;
|
||||
if (difference <= 15 || !session['contract_validation']) {
|
||||
if (difference > 15){
|
||||
difference = 0;
|
||||
bg_color = '#e55e50';
|
||||
}
|
||||
var message = 'Register your contract, only ' + difference + ' days left';
|
||||
var $panel = $(qweb.render('FlectraLicense.contract_expire_panel', {
|
||||
'difference': params.difference,
|
||||
'message': message,
|
||||
'background': bg_color
|
||||
}));
|
||||
$('nav').after($panel);
|
||||
if (difference <= 0) {
|
||||
return self.contract_expired()
|
||||
}
|
||||
$panel.find('#register_contract').bind('click', self.register_contract);
|
||||
}
|
||||
},
|
||||
register_contract: function () {
|
||||
var self = this;
|
||||
var dialog = new Dialog(self).open();
|
||||
dialog.on('get_key', self, function (key) {
|
||||
session.get_file({
|
||||
url: '/flectra/licensing',
|
||||
data: {
|
||||
'binary': key['binary']
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
contract_expired: function () {
|
||||
var self = this;
|
||||
var $message = $('#expiration-message').parent();
|
||||
var $clone = $message.clone();
|
||||
$clone.find('#contract-message').text('Contract Expired !!!').addClass('contract-block');
|
||||
$clone.find('button.close').remove();
|
||||
$message.hide();
|
||||
$clone.find('div#register_contract').after(
|
||||
$('<div id="apply_contract" class="noselect">').append(
|
||||
$('<span id="btn_apply_key">').text('Apply Key')));
|
||||
$clone.find('span#btn_register_contract').off('click').on('click', function () {
|
||||
$.unblockUI();
|
||||
self.register_contract();
|
||||
});
|
||||
$clone.find('#register_contract,#apply_contract').addClass('contract-mrg10');
|
||||
$clone.find('span#btn_apply_key').off('click').on('click', function () {
|
||||
$.unblockUI();
|
||||
rpc.query({
|
||||
model: 'ir.actions.act_window',
|
||||
method: 'search_read',
|
||||
domain: [['context', '=', "{'module' : 'general_settings'}"]]
|
||||
}).done(function (res) {
|
||||
if (!res)
|
||||
window.location.reload();
|
||||
self.do_action(res[0]['id']).done(function () {
|
||||
var $el = $('div[name=activator_key]');
|
||||
if ($el && $el[0]){
|
||||
$el[0].scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||
$el.parents('.o_setting_box').animate({backgroundColor: "rgb(239, 234, 208)"}, 2000, function () {
|
||||
$el.parents('.o_setting_box').animate({backgroundColor: ''})
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
setTimeout(function () {
|
||||
$.blockUI({
|
||||
message: $clone,
|
||||
css: {cursor: 'auto'},
|
||||
overlayCSS: {cursor: 'auto'}
|
||||
});
|
||||
self.contract_expired();
|
||||
}, 15000);
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -134,3 +134,52 @@ div.o_boolean_toggle {
|
||||
.bg-info-full {
|
||||
background-color: @brand-info;
|
||||
}
|
||||
|
||||
#expiration-message {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
|
||||
.alert {
|
||||
padding: 10px !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
#inner-message {
|
||||
margin: 0 auto;
|
||||
#register_contract, #apply_contract {
|
||||
background: #5dae7e;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
span {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contract-block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bg-animate {
|
||||
background-color: rgb(253, 249, 240);
|
||||
}
|
||||
|
||||
.contract-mrg10 {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.noselect {
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none;
|
||||
}
|
||||
|
@ -102,4 +102,31 @@
|
||||
<li class="o_user_bookmark_menu" ><a href="#" title="Bookmark"><i class="fa fa-bookmark"/></a></li>
|
||||
</t>
|
||||
|
||||
|
||||
<div t-name="FlectraLicense.contract_expire_panel" >
|
||||
<div id="expiration-message">
|
||||
<div>
|
||||
<div id="inner-message" class="alert" t-att-style="'background:' + background + ';'">
|
||||
<button type="button" class="close" data-dismiss="alert">&times;</button>
|
||||
<span>
|
||||
<span id="contract-message"><t t-esc="message"/></span>
|
||||
<div id="register_contract" class="noselect">
|
||||
<span id="btn_register_contract">Register</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="FlectraLicense.dialog_contract_registration" >
|
||||
<div id="register_contract_form">
|
||||
<div class="form-group">
|
||||
<label for="contract_id">Contract ID</label>
|
||||
<input type="text" class="form-control" id="contract_id" placeholder="Contract ID"/>
|
||||
<input type="hidden" value=""/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
|
@ -269,6 +269,7 @@
|
||||
|
||||
<script type="text/javascript" src="/web/static/src/js/backend_theme_customizer/backend_theme_customizer.js"/>
|
||||
<script type="text/javascript" src="/web/static/src/js/backend_theme_customizer/customize_switcher.js"/>
|
||||
<script type="text/javascript" src="/web/static/src/js/backend_theme_customizer/DialogRegisterContract.js"/>
|
||||
<script type="text/javascript" src="/web/static/src/js/chrome/bookmark.js"/>
|
||||
|
||||
</template>
|
||||
|
@ -11,6 +11,7 @@ import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import zipfile
|
||||
import datetime,json
|
||||
|
||||
import requests
|
||||
|
||||
@ -28,6 +29,7 @@ from flectra.exceptions import AccessDenied, UserError
|
||||
from flectra.tools.parse_version import parse_version
|
||||
from flectra.tools.misc import topological_sort
|
||||
from flectra.http import request
|
||||
from flectra.addons.web.models.crypt import *
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@ -293,6 +295,7 @@ class Module(models.Model):
|
||||
application = fields.Boolean('Application', readonly=True)
|
||||
icon = fields.Char('Icon URL')
|
||||
icon_image = fields.Binary(string='Icon', compute='_get_icon_image')
|
||||
contract_certificate = fields.Char('Required Contract')
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'UNIQUE (name)', 'The name of the module must be unique!'),
|
||||
@ -433,6 +436,20 @@ class Module(models.Model):
|
||||
"- %s (%s)" % (module.shortdesc, labels[module.state])
|
||||
for module in modules
|
||||
]))
|
||||
ir_config = self.env['ir.config_parameter'].sudo()
|
||||
exp_date = ir_config.get_param('database.expiration_date')
|
||||
reason = ir_config.get_param('database.expiration_reason')
|
||||
set_param = ir_config.set_param
|
||||
|
||||
for mod in self:
|
||||
if mod.contract_certificate and not (reason == 'contract_expire' and exp_date):
|
||||
expire_date = datetime.datetime.now() + datetime.timedelta(days=15)
|
||||
set_param('database.expiration_date', expire_date.replace(microsecond=0))
|
||||
set_param('database.expiration_reason', 'contract_expire')
|
||||
set_param('contract.validity',
|
||||
base64.encodestring(
|
||||
encrypt(json.dumps(str(expire_date.replace(microsecond=0))),
|
||||
str(expire_date.replace(microsecond=0)))))
|
||||
|
||||
return dict(ACTION_DICT, name=_('Install'))
|
||||
|
||||
@ -573,6 +590,13 @@ class Module(models.Model):
|
||||
raise UserError(_("The `base` module cannot be uninstalled"))
|
||||
deps = self.downstream_dependencies()
|
||||
(self + deps).write({'state': 'to remove'})
|
||||
modules = self.env['ir.module.module'].search([('contract_certificate', '!=', False), ('state', '=', 'installed')])
|
||||
ir_config = self.env['ir.config_parameter'].sudo()
|
||||
set_param = ir_config.set_param
|
||||
if len(modules) <= 0:
|
||||
set_param('database.expiration_date', False)
|
||||
set_param('database.expiration_reason', False)
|
||||
set_param('contract.validity', False)
|
||||
return dict(ACTION_DICT, name=_('Uninstall'))
|
||||
|
||||
@assert_log_admin_access
|
||||
@ -697,6 +721,8 @@ class Module(models.Model):
|
||||
updated_values['state'] = 'uninstalled'
|
||||
if parse_version(terp.get('version', default_version)) > parse_version(mod.latest_version or default_version):
|
||||
res[0] += 1
|
||||
if terp.get('contract_certificate'):
|
||||
mod.write({'contract_certificate': terp.get('contract_certificate') or False})
|
||||
if updated_values:
|
||||
mod.write(updated_values)
|
||||
else:
|
||||
|
@ -94,6 +94,7 @@
|
||||
<group col="4">
|
||||
<field name="demo"/>
|
||||
<field name="application"/>
|
||||
<field name="contract_certificate"/>
|
||||
<field name="state"/>
|
||||
</group>
|
||||
<group string="Created Views" attrs="{'invisible':[('state','!=','installed')]}"/>
|
||||
|
@ -45,3 +45,4 @@ xlrd==1.0.0
|
||||
unittest2==1.1.0
|
||||
numpy==1.14.3
|
||||
pypiwin32 ; sys_platform == 'win32'
|
||||
pycrypto==2.6.1
|
Loading…
Reference in New Issue
Block a user