Merge branch 'flectra-license-server' into 'master'

[ADD] support for delayed release

See merge request flectra-hq/flectra!163
This commit is contained in:
Parthiv Patel 2018-10-31 15:00:42 +00:00
commit 35046ba1b9
13 changed files with 376 additions and 3 deletions

View File

@ -2,8 +2,10 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import api, fields, models, _ 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): class ResConfigSettings(models.TransientModel):
@ -37,6 +39,8 @@ class ResConfigSettings(models.TransientModel):
external_report_layout = fields.Selection(related="company_id.external_report_layout") external_report_layout = fields.Selection(related="company_id.external_report_layout")
send_statistics = fields.Boolean( send_statistics = fields.Boolean(
"Send Statistics") "Send Statistics")
activator_key = fields.Binary('Upload Activation Key')
contract_id = fields.Char('Contract ID')
@api.model @api.model
def get_values(self): def get_values(self):
@ -72,6 +76,8 @@ class ResConfigSettings(models.TransientModel):
self.env['ir.config_parameter'].sudo().set_param( self.env['ir.config_parameter'].sudo().set_param(
"base_setup.send_statistics", send_statistics) "base_setup.send_statistics", send_statistics)
self.env.ref('base.res_partner_rule').write({'active': not self.company_share_partner}) self.env.ref('base.res_partner_rule').write({'active': not self.company_share_partner})
if self.activator_key:
self._check_authorization()
@api.multi @api.multi
def open_company(self): def open_company(self):
@ -123,3 +129,22 @@ class ResConfigSettings(models.TransientModel):
'view_id': template.id, 'view_id': template.id,
'target': 'new', '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!'))

View File

@ -203,6 +203,19 @@
</div> </div>
</div> </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> </div>
<h2>System Parameter</h2> <h2>System Parameter</h2>
<div class="row mt16 o_settings_container" id="send_statistics"> <div class="row mt16 o_settings_container" id="send_statistics">

View File

@ -52,6 +52,7 @@ import requests
from flectra.tools import config from flectra.tools import config
from flectra import release from flectra import release
from flectra.http import root from flectra.http import root
from ..models.crypt import *
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -69,6 +70,8 @@ env.filters["json"] = json.dumps
BUNDLE_MAXAGE = 60 * 60 * 24 * 7 BUNDLE_MAXAGE = 60 * 60 * 24 * 7
DBNAME_PATTERN = '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$' DBNAME_PATTERN = '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$'
FILENAME = 'licence'
EXT = 'key'
#---------------------------------------------------------- #----------------------------------------------------------
# Flectra Web helpers # Flectra Web helpers
@ -1861,3 +1864,16 @@ class ReportController(http.Controller):
@http.route(['/report/check_wkhtmltopdf'], type='json', auth="user") @http.route(['/report/check_wkhtmltopdf'], type='json', auth="user")
def check_wkhtmltopdf(self): def check_wkhtmltopdf(self):
return request.env['ir.actions.report'].get_wkhtmltopdf_state() 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))
]
)

View 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:]))

View File

@ -2,9 +2,11 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
import json import json
import base64
from flectra import models from flectra import models,api
from flectra.http import request from flectra.http import request
from .crypt import *
import flectra import flectra
@ -22,6 +24,16 @@ class Http(models.AbstractModel):
user = request.env.user user = request.env.user
display_switch_company_menu = user.has_group('base.group_multi_company') and len(user.company_ids) > 1 display_switch_company_menu = user.has_group('base.group_multi_company') and len(user.company_ids) > 1
version_info = flectra.service.common.exp_version() 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 { return {
"session_id": request.session.sid, "session_id": request.session.sid,
"uid": request.session.uid, "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, "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 {}, "currencies": self.get_currencies() if request.session.uid else {},
"web.base.url": self.env['ir.config_parameter'].sudo().get_param('web.base.url', default=''), "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): def get_currencies(self):
Currency = request.env['res.currency'] Currency = request.env['res.currency']
currencies = Currency.search([]).read(['symbol', 'position', 'decimal_places']) 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} 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

View File

@ -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();
});
}
})
});

View File

@ -11,6 +11,9 @@ var SystrayMenu = require('web.SystrayMenu');
var UserMenu = require('web.UserMenu'); var UserMenu = require('web.UserMenu');
var UserProfile = require('web.UserProfile'); var UserProfile = require('web.UserProfile');
var config = require('web.config'); var config = require('web.config');
var rpc = require('web.rpc');
var qweb = core.qweb;
var Dialog = require('FlectraLicensing.DialogRegisterContract');
return AbstractWebClient.extend({ return AbstractWebClient.extend({
events: { events: {
@ -55,6 +58,9 @@ return AbstractWebClient.extend({
this.systray_menu.setElement(this.$el.parents().find('.oe_systray')); this.systray_menu.setElement(this.$el.parents().find('.oe_systray'));
var systray_menu_loaded = this.systray_menu.start(); 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 // Start the menu once both systray and user menus are rendered
// to prevent overflows while loading // to prevent overflows while loading
return $.when(systray_menu_loaded, user_menu_loaded).then(function() { return $.when(systray_menu_loaded, user_menu_loaded).then(function() {
@ -198,6 +204,95 @@ return AbstractWebClient.extend({
this.menu.reflow(); 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);
},
}); });
}); });

View File

@ -134,3 +134,52 @@ div.o_boolean_toggle {
.bg-info-full { .bg-info-full {
background-color: @brand-info; 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;
}

View File

@ -102,4 +102,31 @@
<li class="o_user_bookmark_menu" ><a href="#" title="Bookmark"><i class="fa fa-bookmark"/></a></li> <li class="o_user_bookmark_menu" ><a href="#" title="Bookmark"><i class="fa fa-bookmark"/></a></li>
</t> </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">&amp;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> </templates>

View File

@ -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/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/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"/> <script type="text/javascript" src="/web/static/src/js/chrome/bookmark.js"/>
</template> </template>

View File

@ -11,6 +11,7 @@ import os
import shutil import shutil
import tempfile import tempfile
import zipfile import zipfile
import datetime,json
import requests import requests
@ -28,6 +29,7 @@ from flectra.exceptions import AccessDenied, UserError
from flectra.tools.parse_version import parse_version from flectra.tools.parse_version import parse_version
from flectra.tools.misc import topological_sort from flectra.tools.misc import topological_sort
from flectra.http import request from flectra.http import request
from flectra.addons.web.models.crypt import *
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -293,6 +295,7 @@ class Module(models.Model):
application = fields.Boolean('Application', readonly=True) application = fields.Boolean('Application', readonly=True)
icon = fields.Char('Icon URL') icon = fields.Char('Icon URL')
icon_image = fields.Binary(string='Icon', compute='_get_icon_image') icon_image = fields.Binary(string='Icon', compute='_get_icon_image')
contract_certificate = fields.Char('Required Contract')
_sql_constraints = [ _sql_constraints = [
('name_uniq', 'UNIQUE (name)', 'The name of the module must be unique!'), ('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]) "- %s (%s)" % (module.shortdesc, labels[module.state])
for module in modules 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')) return dict(ACTION_DICT, name=_('Install'))
@ -573,6 +590,13 @@ class Module(models.Model):
raise UserError(_("The `base` module cannot be uninstalled")) raise UserError(_("The `base` module cannot be uninstalled"))
deps = self.downstream_dependencies() deps = self.downstream_dependencies()
(self + deps).write({'state': 'to remove'}) (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')) return dict(ACTION_DICT, name=_('Uninstall'))
@assert_log_admin_access @assert_log_admin_access
@ -697,6 +721,8 @@ class Module(models.Model):
updated_values['state'] = 'uninstalled' updated_values['state'] = 'uninstalled'
if parse_version(terp.get('version', default_version)) > parse_version(mod.latest_version or default_version): if parse_version(terp.get('version', default_version)) > parse_version(mod.latest_version or default_version):
res[0] += 1 res[0] += 1
if terp.get('contract_certificate'):
mod.write({'contract_certificate': terp.get('contract_certificate') or False})
if updated_values: if updated_values:
mod.write(updated_values) mod.write(updated_values)
else: else:

View File

@ -94,6 +94,7 @@
<group col="4"> <group col="4">
<field name="demo"/> <field name="demo"/>
<field name="application"/> <field name="application"/>
<field name="contract_certificate"/>
<field name="state"/> <field name="state"/>
</group> </group>
<group string="Created Views" attrs="{'invisible':[('state','!=','installed')]}"/> <group string="Created Views" attrs="{'invisible':[('state','!=','installed')]}"/>

View File

@ -45,3 +45,4 @@ xlrd==1.0.0
unittest2==1.1.0 unittest2==1.1.0
numpy==1.14.3 numpy==1.14.3
pypiwin32 ; sys_platform == 'win32' pypiwin32 ; sys_platform == 'win32'
pycrypto==2.6.1