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.
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!'))

View File

@ -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">

View File

@ -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))
]
)

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.
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

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 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);
},
});
});

View File

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

View File

@ -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">&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>

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/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>

View File

@ -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:

View File

@ -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')]}"/>

View File

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