class Invoice(metaclass=PoolMeta): __name__ = 'account.invoice' numbered_at = fields.Timestamp("Numbered At") @classmethod def __register__(cls, module_name): super().__register__(module_name) table_h = cls.__table_handler__(module_name) table = cls.__table__() cursor = Transaction().connection.cursor() # Migration from 5.2: rename open_date into numbered_at if table_h.column_exist('open_date'): cursor.execute( *table.update( [table.numbered_at], [table.open_date])) table_h.drop_column('open_date') @classmethod def __setup__(cls): super(Invoice, cls).__setup__() cls._check_modify_exclude.append('numbered_at') cls.party.datetime_field = 'numbered_at' if 'numbered_at' not in cls.party.depends: cls.party.depends.append('numbered_at') cls.invoice_address.datetime_field = 'numbered_at' if 'numbered_at' not in cls.invoice_address.depends: cls.invoice_address.depends.append('numbered_at') cls.payment_term.datetime_field = 'numbered_at' if 'numbered_at' not in cls.payment_term.depends: cls.payment_term.depends.append('numbered_at') @classmethod def set_number(cls, invoices): numbered = [i for i in invoices if not i.number or not i.numbered_at] super(Invoice, cls).set_number(invoices) if numbered: cls.write(numbered, { 'numbered_at': CurrentTimestamp(), }) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, invoices): super(Invoice, cls).draft(invoices) cls.write(invoices, { 'numbered_at': None, }) @classmethod def copy(cls, invoices, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('numbered_at', None) return super(Invoice, cls).copy(invoices, default=default)
def get_fields(self): # TODO: Cache Data = Pool().get('lims.interface.data') table = Data.get_table() if not table: return Data._previous_fields res = {} groups = 0 for field in table.fields_: if field.type == 'char': obj = fields.Char(field.string) elif field.type == 'multiline': obj = fields.Text(field.string) elif field.type == 'integer': obj = fields.Integer(field.string) elif field.type == 'float': obj = fields.Float(field.string) elif field.type == 'boolean': obj = fields.Boolean(field.string) elif field.type == 'numeric': obj = fields.Numeric(field.string) elif field.type == 'date': obj = fields.Date(field.string) elif field.type == 'datetime': obj = fields.DateTime(field.string) elif field.type == 'timestamp': obj = fields.Timestamp(field.string) elif field.type == 'many2one': obj = fields.Many2One(field.related_model.model, field.string) elif field.type in ('binary', 'icon'): obj = fields.Binary(field.string) elif field.type == 'selection': selection = [tuple(v.split(':', 1)) for v in field.selection.splitlines() if v] obj = fields.Selection(selection, field.string) obj.name = field.name res[field.name] = obj groups = max(groups, field.group or 0) obj = fields.Integer('ID') obj.name = 'id' res['id'] = obj obj = fields.Many2One('lims.interface.compilation', 'Compilation') obj.name = 'compilation' res['compilation'] = obj obj = fields.Boolean('Annulled') obj.name = 'annulled' res['annulled'] = obj obj = fields.Many2One('lims.notebook.line', 'Notebook Line') obj.name = 'notebook_line' obj.readonly = True res['notebook_line'] = obj for i in range(0, groups): obj = fields.One2Many( 'lims.interface.grouped_data', 'data', 'Group %s' % (i + 1, )) obj.name = 'group_%s' % (i + 1, ) res[obj.name] = obj return res
class TestHistory(ModelSQL): 'Test History' __name__ = 'test.history' _history = True value = fields.Integer('Value') lines = fields.One2Many('test.history.line', 'history', 'Lines') lines_at_stamp = fields.One2Many('test.history.line', 'history', 'Lines at Stamp', datetime_field='stamp') stamp = fields.Timestamp('Stamp')
def get_fields(self): GroupedData = Pool().get('lims.interface.grouped_data') table = GroupedData.get_table() if not table: return GroupedData._previous_fields res = {} for field in table.grouped_fields_: if field.type == 'char': obj = fields.Char(field.string) elif field.type == 'multiline': obj = fields.Text(field.string) elif field.type == 'integer': obj = fields.Integer(field.string) elif field.type == 'float': obj = fields.Float(field.string) elif field.type == 'boolean': obj = fields.Boolean(field.string) elif field.type == 'numeric': obj = fields.Numeric(field.string) elif field.type == 'date': obj = fields.Date(field.string) elif field.type == 'datetime': obj = fields.DateTime(field.string) elif field.type == 'timestamp': obj = fields.Timestamp(field.string) elif field.type == 'many2one': obj = fields.Many2One(field.related_model.model, field.string) elif field.type in ('binary', 'icon'): obj = fields.Binary(field.string) elif field.type == 'selection': selection = [tuple(v.split(':', 1)) for v in field.selection.splitlines() if v] obj = fields.Selection(selection, field.string) obj.name = field.name res[field.name] = obj obj = fields.Integer('ID') obj.name = 'id' res['id'] = obj obj = fields.Many2One('lims.notebook.line', 'Notebook Line') obj.name = 'notebook_line' obj.readonly = True res['notebook_line'] = obj obj = fields.Many2One('lims.interface.data', 'Data') obj.name = 'data' obj.readonly = True res['data'] = obj obj = fields.Integer('Iteration') obj.name = 'iteration' obj.readonly = True res['iteration'] = obj return res
def get_fields(self): # TODO: Cache Data = Pool().get('shine.data') table = Data.get_table() if not table: return Data._previous_fields res = {} for field in table.fields: if field.type == 'char': obj = fields.Char(field.string) elif field.type == 'multiline': obj = fields.Text(field.string) elif field.type == 'integer': obj = fields.Integer(field.string) elif field.type == 'float': obj = fields.Float(field.string) elif field.type == 'boolean': obj = fields.Boolean(field.string) elif field.type == 'numeric': obj = fields.Numeric(field.string) elif field.type == 'date': obj = fields.Date(field.string) elif field.type == 'datetime': obj = fields.DateTime(field.string) elif field.type == 'timestamp': obj = fields.Timestamp(field.string) elif field.type == 'many2one': obj = fields.Many2One(field.related_model.model, field.string) elif field.type in ('binary', 'icon'): obj = fields.Binary(field.string) obj.name = field.name res[field.name] = obj if not 'id' in res: obj = fields.Integer('ID') obj.name = 'id' res[field.name] = obj return res
class Queue(ModelSQL): "Queue" __name__ = 'ir.queue' name = fields.Char("Name", required=True) data = fields.Dict(None, "Data") enqueued_at = fields.Timestamp("Enqueued at", required=True) dequeued_at = fields.Timestamp("Dequeued at") finished_at = fields.Timestamp("Finished at") scheduled_at = fields.Timestamp("Scheduled at", help="When the task can start.") expected_at = fields.Timestamp("Expected at", help="When the task should be done.") @classmethod def __register__(cls, module_name): queue = cls.__table__() super().__register__(module_name) table_h = cls.__table_handler__(module_name) # Add index for candidates table_h.index_action([ queue.scheduled_at.nulls_first, queue.expected_at.nulls_first, queue.dequeued_at, queue.name, ], action='add') @classmethod def default_enqueued_at(cls): return datetime.datetime.now() @classmethod def copy(cls, records, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('enqueued_at') default.setdefault('dequeued_at') default.setdefault('finished_at') return super(Queue, cls).copy(records, default=default) @classmethod def push(cls, name, data, scheduled_at=None, expected_at=None): transaction = Transaction() database = transaction.database cursor = transaction.connection.cursor() with transaction.set_user(0): record, = cls.create([{ 'name': name, 'data': data, 'scheduled_at': scheduled_at, 'expected_at': expected_at, }]) if database.has_channel(): cursor.execute('NOTIFY "%s"', (cls.__name__,)) if not has_worker: transaction.tasks.append(record.id) return record.id @classmethod def pull(cls, database, connection, name=None): cursor = connection.cursor() queue = cls.__table__() candidates = With('id', 'scheduled_at', 'expected_at', query=queue.select( queue.id, queue.scheduled_at, queue.expected_at, where=((queue.name == name) if name else Literal(True)) & (queue.dequeued_at == Null), order_by=[ queue.scheduled_at.nulls_first, queue.expected_at.nulls_first])) selected = With('id', query=candidates.select( candidates.id, where=((candidates.scheduled_at <= CurrentTimestamp()) | (candidates.scheduled_at == Null)) & database.lock_id(candidates.id), order_by=[ candidates.scheduled_at.nulls_first, candidates.expected_at.nulls_first], limit=1)) next_timeout = With('seconds', query=candidates.select( Min(Extract('second', candidates.scheduled_at - CurrentTimestamp()) ), where=candidates.scheduled_at >= CurrentTimestamp())) task_id, seconds = None, None if database.has_returning(): query = queue.update([queue.dequeued_at], [CurrentTimestamp()], where=queue.id == selected.select(selected.id), with_=[candidates, selected, next_timeout], returning=[ queue.id, next_timeout.select(next_timeout.seconds)]) cursor.execute(*query) row = cursor.fetchone() if row: task_id, seconds = row else: query = queue.select(queue.id, where=queue.id == selected.select(selected.id), with_=[candidates, selected]) cursor.execute(*query) row = cursor.fetchone() if row: task_id, = row query = queue.update([queue.dequeued_at], [CurrentTimestamp()], where=queue.id == task_id) cursor.execute(*query) query = next_timeout.select(next_timeout.seconds) cursor.execute(*query) row = cursor.fetchone() if row: seconds, = row if not task_id and database.has_channel(): cursor.execute('LISTEN "%s"', (cls.__name__,)) return task_id, seconds def run(self): transaction = Transaction() Model = Pool().get(self.data['model']) with transaction.set_user(self.data['user']), \ transaction.set_context(self.data['context']): instances = self.data['instances'] # Ensure record ids still exist if isinstance(instances, int): with transaction.set_context(active_test=False): if Model.search([('id', '=', instances)]): instances = Model(instances) else: instances = None else: ids = set() with transaction.set_context(active_test=False): for sub_ids in grouped_slice(instances): records = Model.search([('id', 'in', list(sub_ids))]) ids.update(map(int, records)) if ids: instances = Model.browse( [i for i in instances if i in ids]) else: instances = None if instances is not None: getattr(Model, self.data['method'])( instances, *self.data['args'], **self.data['kwargs']) if not self.dequeued_at: self.dequeued_at = datetime.datetime.now() self.finished_at = datetime.datetime.now() self.save() @classmethod def caller(cls, model): return _Model(cls, model)
class User(avatar_mixin(100), DeactivableMixin, ModelSQL, ModelView): 'Web User' __name__ = 'web.user' _rec_name = 'email' email = fields.Char('E-mail', select=True, states={ 'required': Eval('active', True), }) email_valid = fields.Boolean('E-mail Valid') email_token = fields.Char('E-mail Token', select=True) password_hash = fields.Char('Password Hash') password = fields.Function( fields.Char('Password'), 'get_password', setter='set_password') reset_password_token = fields.Char('Reset Password Token', select=True) reset_password_token_expire = fields.Timestamp( 'Reset Password Token Expire') party = fields.Many2One('party.party', 'Party', ondelete='RESTRICT') secondary_parties = fields.Many2Many( 'web.user-party.party.secondary', 'user', 'party', "Secondary Parties") @classmethod def __setup__(cls): super(User, cls).__setup__() table = cls.__table__() cls._sql_constraints += [ ('email_exclude', Exclude(table, (table.email, Equal), where=table.active == Literal(True)), 'web_user.msg_user_email_unique'), ] cls._buttons.update({ 'validate_email': { 'readonly': Eval('email_valid', False), 'depends': ['email_valid'], }, 'reset_password': { 'readonly': ~Eval('email_valid', False), 'depends': ['email_valid'], }, }) @classmethod def __register__(cls, module_name): super(User, cls).__register__(module_name) table_h = cls.__table_handler__(module_name) # Migration from 4.6 table_h.not_null_action('email', 'remove') # Migration from 4.6: replace unique by exclude table_h.drop_constraint('email_unique') @classmethod def default_email_valid(cls): return False def get_password(self, name): return 'x' * 10 @classmethod def set_password(cls, users, name, value): pool = Pool() User = pool.get('res.user') if value == 'x' * 10: return if Transaction().user and value: User.validate_password(value, users) to_write = [] for user in users: to_write.extend([[user], { 'password_hash': cls.hash_password(value), }]) cls.write(*to_write) @fields.depends('party', 'email') def on_change_party(self): if not self.email and self.party: self.email = self.party.email @classmethod def _format_email(cls, users): for user in users: email = user.email.lower() if email != user.email: user.email = email cls.save(users) @classmethod def create(cls, vlist): users = super(User, cls).create(vlist) cls._format_email(users) return users @classmethod def write(cls, *args): super(User, cls).write(*args) users = sum(args[0:None:2], []) cls._format_email(users) @classmethod def authenticate(cls, email, password): pool = Pool() Attempt = pool.get('web.user.authenticate.attempt') email = email.lower() count_ip = Attempt.count_ip() if count_ip > config.getint( 'session', 'max_attempt_ip_network', default=300): # Do not add attempt as the goal is to prevent flooding raise RateLimitException() count = Attempt.count(email) if count > config.getint('session', 'max_attempt', default=5): Attempt.add(email) raise RateLimitException() # Prevent brute force attack Transaction().atexit(time.sleep, 2 ** count - 1) users = cls.search([('email', '=', email)]) if users: user, = users valid, new_hash = cls.check_password(password, user.password_hash) if valid: if new_hash: logger.info("Update password hash for %s", user.id) with Transaction().new_transaction() as transaction: with transaction.set_user(0): cls.write([cls(user.id)], { 'password_hash': new_hash, }) Attempt.remove(email) return user Attempt.add(email) @classmethod def hash_password(cls, password): '''Hash given password in the form <hash_method>$<password>$<salt>...''' if not password: return '' return CRYPT_CONTEXT.hash(password) @classmethod def check_password(cls, password, hash_): if not hash_: return False, None try: return CRYPT_CONTEXT.verify_and_update(password, hash_) except ValueError: hash_method = hash_.split('$', 1)[0] warnings.warn( "Use deprecated hash method %s" % hash_method, DeprecationWarning) valid = getattr(cls, 'check_' + hash_method)(password, hash_) if valid: new_hash = CRYPT_CONTEXT.hash(password) else: new_hash = None return valid, new_hash @classmethod def hash_sha1(cls, password): salt = ''.join(random.sample(string.ascii_letters + string.digits, 8)) salted_password = password + salt if isinstance(salted_password, str): salted_password = salted_password.encode('utf-8') hash_ = hashlib.sha1(salted_password).hexdigest() return '$'.join(['sha1', hash_, salt]) @classmethod def check_sha1(cls, password, hash_): if isinstance(password, str): password = password.encode('utf-8') hash_method, hash_, salt = hash_.split('$', 2) salt = salt or '' if isinstance(salt, str): salt = salt.encode('utf-8') assert hash_method == 'sha1' return hash_ == hashlib.sha1(password + salt).hexdigest() @classmethod def hash_bcrypt(cls, password): if isinstance(password, str): password = password.encode('utf-8') hash_ = bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8') return '$'.join(['bcrypt', hash_]) @classmethod def check_bcrypt(cls, password, hash_): if isinstance(password, str): password = password.encode('utf-8') hash_method, hash_ = hash_.split('$', 1) if isinstance(hash_, str): hash_ = hash_.encode('utf-8') assert hash_method == 'bcrypt' return hash_ == bcrypt.hashpw(password, hash_) def new_session(self): pool = Pool() Session = pool.get('web.user.session') return Session.add(self) @classmethod def get_user(cls, session): pool = Pool() Session = pool.get('web.user.session') return Session.get_user(session) @classmethod @ModelView.button def validate_email(cls, users, from_=None): for user in users: user.set_email_token() cls.save(users) _send_email(from_, users, cls.get_email_validation) def set_email_token(self, nbytes=None): self.email_token = token_hex(nbytes) def get_email_validation(self): return get_email( 'web.user.email_validation', self, self.languages) def get_email_validation_url(self, url=None): if url is None: url = config.get('web', 'email_validation_url') return _add_params(url, token=self.email_token) @classmethod def validate_email_url(cls, url): parts = urllib.parse.urlsplit(url) tokens = filter( None, urllib.parse.parse_qs(parts.query).get('token', [None])) return cls.validate_email_token(list(tokens)) @classmethod def validate_email_token(cls, tokens): users = cls.search([ ('email_token', 'in', tokens), ]) cls.write(users, { 'email_valid': True, 'email_token': None, }) return users @classmethod @ModelView.button def reset_password(cls, users, from_=None): now = datetime.datetime.now() # Prevent abusive reset def reset(user): return not (user.reset_password_token_expire and user.reset_password_token_expire > now) users = list(filter(reset, users)) for user in users: user.set_reset_password_token() cls.save(users) _send_email(from_, users, cls.get_email_reset_password) def set_reset_password_token(self, nbytes=None): self.reset_password_token = token_hex(nbytes) self.reset_password_token_expire = ( datetime.datetime.now() + datetime.timedelta( seconds=config.getint( 'session', 'web_timeout_reset', default=24 * 60 * 60))) def clear_reset_password_token(self): self.reset_password_token = None self.reset_password_token_expire = None def get_email_reset_password(self): return get_email( 'web.user.email_reset_password', self, self.languages) def get_email_reset_password_url(self, url=None): if url is None: url = config.get('web', 'reset_password_url') return _add_params( url, token=self.reset_password_token, email=self.email) @classmethod def set_password_url(cls, url, password): parts = urllib.parse.urlsplit(url) query = urllib.parse.parse_qs(parts.query) email = query.get('email', [None])[0] token = query.get('token', [None])[0] return cls.set_password_token(email, token, password) @classmethod def set_password_token(cls, email, token, password): pool = Pool() Attempt = pool.get('web.user.authenticate.attempt') email = email.lower() # Prevent brute force attack Transaction().atexit( time.sleep, random.randint(0, 2 ** Attempt.count(email) - 1)) users = cls.search([ ('email', '=', email), ]) if users: user, = users if user.reset_password_token == token: now = datetime.datetime.now() expire = user.reset_password_token_expire user.clear_reset_password_token() if expire > now: user.password = password user.save() Attempt.remove(email) return True Attempt.add(email) return False @property def languages(self): pool = Pool() Language = pool.get('ir.lang') if self.party and self.party.lang: languages = [self.party.lang] else: languages = Language.search([ ('code', '=', Transaction().language), ]) return languages
class User(avatar_mixin(100, 'login'), DeactivableMixin, ModelSQL, ModelView): "User" __name__ = "res.user" name = fields.Char('Name', select=True) login = fields.Char('Login', required=True) password_hash = fields.Char('Password Hash') password = fields.Function(fields.Char("Password", states={ 'invisible': not _has_password, }), getter='get_password', setter='set_password') password_reset = fields.Char("Reset Password", states={ 'invisible': not _has_password, }) password_reset_expire = fields.Timestamp("Reset Password Expire", states={ 'required': Bool(Eval('password_reset')), 'invisible': not _has_password, }, depends=['password_reset']) signature = fields.Text('Signature') menu = fields.Many2One('ir.action', 'Menu Action', domain=[('usage', '=', 'menu')], required=True) pyson_menu = fields.Function(fields.Char('PySON Menu'), 'get_pyson_menu') actions = fields.Many2Many('res.user-ir.action', 'user', 'action', 'Actions', help='Actions that will be run at login.', size=5) groups = fields.Many2Many('res.user-res.group', 'user', 'group', 'Groups') applications = fields.One2Many('res.user.application', 'user', "Applications") language = fields.Many2One('ir.lang', 'Language', domain=[ 'OR', ('translatable', '=', True), ]) language_direction = fields.Function(fields.Char('Language Direction'), 'get_language_direction') email = fields.Char('Email') status_bar = fields.Function(fields.Char('Status Bar'), 'get_status_bar') avatar_badge_url = fields.Function(fields.Char("Avatar Badge URL"), 'get_avatar_badge_url') warnings = fields.One2Many('res.user.warning', 'user', 'Warnings') sessions = fields.Function(fields.Integer('Sessions'), 'get_sessions') _get_preferences_cache = Cache('res_user.get_preferences') _get_groups_cache = Cache('res_user.get_groups', context=False) _get_login_cache = Cache('res_user._get_login', context=False) @classmethod def __setup__(cls): super(User, cls).__setup__() cls.__rpc__.update({ 'get_preferences': RPC(check_access=False), 'set_preferences': RPC(readonly=False, check_access=False, fresh_session=True), 'get_preferences_fields_view': RPC(check_access=False), }) table = cls.__table__() cls._sql_constraints += [ ('login_key', Unique(table, table.login), 'You can not have two users with the same login!') ] cls._buttons.update({ 'reset_password': { 'invisible': ~Eval('email', True) | (not _has_password), }, }) cls._preferences_fields = [ 'name', 'password', 'email', 'signature', 'menu', 'pyson_menu', 'actions', 'status_bar', 'avatar', 'avatar_url', 'avatar_badge_url', 'warnings', 'applications', ] cls._context_fields = [ 'language', 'language_direction', 'groups', ] cls._order.insert(0, ('name', 'ASC')) @classmethod def __register__(cls, module_name): pool = Pool() ModelData = pool.get('ir.model.data') model_data = ModelData.__table__() cursor = Transaction().connection.cursor() super(User, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 3.0 if table.column_exist('password') and table.column_exist('salt'): sqltable = cls.__table__() password_hash_new = Concat( 'sha1$', Concat(sqltable.password, Concat('$', Coalesce(sqltable.salt, '')))) cursor.execute(*sqltable.update(columns=[sqltable.password_hash], values=[password_hash_new])) table.drop_column('password') table.drop_column('salt') # Migration from 4.2: Remove required on name table.not_null_action('name', action='remove') # Migration from 5.6: Set noupdate to admin cursor.execute( *model_data.update([model_data.noupdate], [True], where=(model_data.model == cls.__name__) & (model_data.module == 'res') & (model_data.fs_id == 'user_admin'))) @staticmethod def default_menu(): pool = Pool() Action = pool.get('ir.action') actions = Action.search([ ('usage', '=', 'menu'), ], limit=1) if actions: return actions[0].id return None def get_pyson_menu(self, name): encoder = PYSONEncoder() return encoder.encode(self.menu.get_action_value()) def get_language_direction(self, name): pool = Pool() Lang = pool.get('ir.lang') if self.language: return self.language.direction else: return Lang.default_direction() def get_status_bar(self, name): return self.name def get_avatar_badge_url(self, name): pass def get_password(self, name): return 'x' * 10 @classmethod def set_password(cls, users, name, value): if value == 'x' * 10: return if Transaction().user and value: cls.validate_password(value, users) to_write = [] for user in users: to_write.extend([[user], { 'password_hash': cls.hash_password(value), }]) cls.write(*to_write) @classmethod def validate_password(cls, password, users): password_b = password if isinstance(password, str): password_b = password.encode('utf-8') length = config.getint('password', 'length', default=0) if length > 0: if len(password_b) < length: raise PasswordError( gettext( 'res.msg_password_length', length=length, )) path = config.get('password', 'forbidden', default=None) if path: with open(path, 'r') as f: forbidden = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) if forbidden.find(password_b) >= 0: raise PasswordError(gettext('res.msg_password_forbidden')) entropy = config.getfloat('password', 'entropy', default=0) if entropy: if len(set(password)) / len(password) < entropy: raise PasswordError(gettext('res.msg_password_entropy')) for user in users: # Use getattr to allow to use non User instances for test, message in [ (getattr(user, 'name', ''), 'res.msg_password_name'), (getattr(user, 'login', ''), 'res.msg_password_login'), (getattr(user, 'email', ''), 'res.msg_password_email'), ]: if test and password.lower() == test.lower(): raise PasswordError(gettext(message)) @classmethod @ModelView.button def reset_password(cls, users, length=8, from_=None): for user in users: user.password_reset = gen_password(length=length) user.password_reset_expire = ( datetime.datetime.now() + datetime.timedelta( seconds=config.getint('password', 'reset_timeout'))) user.password = None cls.save(users) _send_email(from_, users, cls.get_email_reset_password) def get_email_reset_password(self): return get_email('res.user.email_reset_password', self, self.languages) @property def languages(self): pool = Pool() Lang = pool.get('ir.lang') if self.language: languages = [self.language] else: languages = Lang.search([ ('code', '=', Transaction().language), ]) return languages @staticmethod def get_sessions(users, name): Session = Pool().get('ir.session') now = datetime.datetime.now() timeout = datetime.timedelta( seconds=config.getint('session', 'max_age')) result = dict((u.id, 0) for u in users) with Transaction().set_user(0): for sub_ids in grouped_slice(users): sessions = Session.search([ ('create_uid', 'in', sub_ids), ], order=[('create_uid', 'ASC')]) def filter_(session): timestamp = session.write_date or session.create_date return abs(timestamp - now) < timeout result.update( dict((i, len(list(g))) for i, g in groupby(filter(filter_, sessions), attrgetter('create_uid.id')))) return result @staticmethod def _convert_vals(vals): vals = vals.copy() pool = Pool() Action = pool.get('ir.action') if 'menu' in vals: vals['menu'] = Action.get_action_id(vals['menu']) return vals @classmethod def read(cls, ids, fields_names): result = super(User, cls).read(ids, fields_names) if not fields_names or 'password_hash' in fields_names: for values in result: values['password_hash'] = None return result @classmethod def create(cls, vlist): vlist = [cls._convert_vals(vals) for vals in vlist] res = super(User, cls).create(vlist) # Restart the cache for _get_login cls._get_login_cache.clear() return res @classmethod def write(cls, users, values, *args): pool = Pool() Session = pool.get('ir.session') UserDevice = pool.get('res.user.device') actions = iter((users, values) + args) all_users = [] session_to_clear = [] users_to_clear = [] args = [] for users, values in zip(actions, actions): all_users += users args.extend((users, cls._convert_vals(values))) if 'password' in values: session_to_clear += users users_to_clear += [u.login for u in users] super(User, cls).write(*args) Session.clear(session_to_clear) UserDevice.clear(users_to_clear) # Clean cursor cache as it could be filled by domain_get for cache in Transaction().cache.values(): if cls.__name__ in cache: for user in all_users: cache[cls.__name__].pop(user.id, None) # Restart the cache for domain_get method pool = Pool() pool.get('ir.rule')._domain_get_cache.clear() # Restart the cache for get_groups cls._get_groups_cache.clear() # Restart the cache for _get_login cls._get_login_cache.clear() # Restart the cache for get_preferences cls._get_preferences_cache.clear() # Restart the cache of check pool.get('ir.model.access')._get_access_cache.clear() # Restart the cache of check pool.get('ir.model.field.access')._get_access_cache.clear() # Restart the cache ModelView._fields_view_get_cache.clear() @classmethod def delete(cls, users): raise DeleteError(gettext('res.msg_user_delete_forbidden')) def get_rec_name(self, name): return self.name if self.name else self.login @classmethod def search_rec_name(cls, name, clause): if clause[1].startswith('!') or clause[1].startswith('not '): bool_op = 'AND' else: bool_op = 'OR' return [ bool_op, ('login', ) + tuple(clause[1:]), ('name', ) + tuple(clause[1:]), ] @classmethod def copy(cls, users, default=None): if default is None: default = {} default = default.copy() default['password'] = '' default.setdefault('warnings') default.setdefault('applications') new_users = [] for user in users: default['login'] = user.login + ' (copy)' new_user, = super(User, cls).copy([user], default) new_users.append(new_user) return new_users @classmethod def _get_preferences(cls, user, context_only=False): pool = Pool() ModelData = pool.get('ir.model.data') Action = pool.get('ir.action') Config = pool.get('ir.configuration') ConfigItem = pool.get('ir.module.config_wizard.item') Lang = pool.get('ir.lang') res = {} if context_only: fields = cls._context_fields else: fields = cls._preferences_fields + cls._context_fields for field in fields: if cls._fields[field]._type in ('many2one', ): if field == 'language': if user.language: res['language'] = user.language.code else: res['language'] = Config.get_language() else: if getattr(user, field): res[field] = getattr(user, field).id res[field + '.rec_name'] = \ getattr(user, field).rec_name elif cls._fields[field]._type in ('one2many', 'many2many'): res[field] = [x.id for x in getattr(user, field)] admin_id = ModelData.get_id('res.user_admin') if field == 'actions' and user.id == admin_id: config_wizard_id = ModelData.get_id( 'ir', 'act_module_config_wizard') action_id = Action.get_action_id(config_wizard_id) if action_id in res[field]: res[field].remove(action_id) if ConfigItem.search([ ('state', '=', 'open'), ]): res[field].insert(0, action_id) else: res[field] = getattr(user, field) if user.language: language = user.language else: try: language = Lang.get(Config.get_language()) except ValueError: language = None if language: date = language.date for i, j in [('%a', ''), ('%A', ''), ('%b', '%m'), ('%B', '%m'), ('%j', ''), ('%U', ''), ('%w', ''), ('%W', '')]: date = date.replace(i, j) res['locale'] = { 'date': date, 'grouping': literal_eval(language.grouping), 'decimal_point': language.decimal_point, 'thousands_sep': language.thousands_sep, 'mon_grouping': literal_eval(language.mon_grouping), 'mon_decimal_point': language.mon_decimal_point, 'mon_thousands_sep': language.mon_thousands_sep, 'p_sign_posn': language.p_sign_posn, 'n_sign_posn': language.n_sign_posn, 'positive_sign': language.positive_sign, 'negative_sign': language.negative_sign, 'p_cs_precedes': language.p_cs_precedes, 'n_cs_precedes': language.n_cs_precedes, 'p_sep_by_space': language.p_sep_by_space, 'n_sep_by_space': language.n_sep_by_space, } return res @classmethod def get_preferences(cls, context_only=False): key = (Transaction().user, context_only) preferences = cls._get_preferences_cache.get(key) if preferences is not None: return preferences.copy() user = Transaction().user user = cls(user) preferences = cls._get_preferences(user, context_only=context_only) cls._get_preferences_cache.set(key, preferences) return preferences.copy() @classmethod def set_preferences(cls, values): ''' Set user preferences ''' pool = Pool() Lang = pool.get('ir.lang') values_clean = values.copy() fields = cls._preferences_fields + cls._context_fields user_id = Transaction().user user = cls(user_id) for field in values: if field not in fields or field == 'groups': del values_clean[field] if field == 'language': langs = Lang.search([ ('code', '=', values['language']), ]) if langs: values_clean['language'] = langs[0].id else: del values_clean['language'] # Set new context to write as validation could depend on it context = {} for name in cls._context_fields: if name in values: context[name] = values[name] with Transaction().set_context(context): cls.write([user], values_clean) @classmethod def get_preferences_fields_view(cls): pool = Pool() ModelData = pool.get('ir.model.data') Lang = pool.get('ir.lang') Action = pool.get('ir.action') view_id = ModelData.get_id('res', 'user_view_form_preferences') res = cls.fields_view_get(view_id=view_id) res = copy.deepcopy(res) for field in res['fields']: if field not in ('groups', 'language_direction'): res['fields'][field]['readonly'] = False else: res['fields'][field]['readonly'] = True def convert2selection(definition, name): del definition[name]['relation'] del definition[name]['domain'] definition[name]['type'] = 'selection' selection = [] definition[name]['selection'] = selection return selection if 'language' in res['fields']: selection = convert2selection(res['fields'], 'language') langs = Lang.search(cls.language.domain) lang_ids = [l.id for l in langs] with Transaction().set_context(translate_name=True): for lang in Lang.browse(lang_ids): selection.append((lang.code, lang.name)) if 'action' in res['fields']: selection = convert2selection(res['fields'], 'action') selection.append((None, '')) actions = Action.search([]) for action in actions: selection.append((action.id, action.rec_name)) if 'menu' in res['fields']: selection = convert2selection(res['fields'], 'menu') actions = Action.search([ ('usage', '=', 'menu'), ]) for action in actions: selection.append((action.id, action.rec_name)) return res @classmethod def get_groups(cls, name=None): ''' Return a list of all group ids for the user ''' user = Transaction().user groups = cls._get_groups_cache.get(user) if groups is not None: return groups pool = Pool() UserGroup = pool.get('res.user-res.group') cursor = Transaction().connection.cursor() user_group = UserGroup.user_group_all_table() cursor.execute(*user_group.select(user_group.group, where=user_group.user == user)) groups = [g for g, in cursor] cls._get_groups_cache.set(user, groups) return groups @classmethod def _get_login(cls, login): result = cls._get_login_cache.get(login) if result: return result cursor = Transaction().connection.cursor() table = cls.__table__() cursor.execute( *table.select(table.id, table.password_hash, Case(( table.password_reset_expire > CurrentTimestamp(), table.password_reset), else_=None), where=(table.login == login) & (table.active == Literal(True)))) result = cursor.fetchone() or (None, None, None) cls._get_login_cache.set(login, result) return result @classmethod def get_login(cls, login, parameters): ''' Return user id if password matches ''' pool = Pool() LoginAttempt = pool.get('res.user.login.attempt') UserDevice = pool.get('res.user.device') count_ip = LoginAttempt.count_ip() if count_ip > config.getint( 'session', 'max_attempt_ip_network', default=300): # Do not add attempt as the goal is to prevent flooding raise RateLimitException() device_cookie = UserDevice.get_valid_cookie( login, parameters.get('device_cookie')) count = LoginAttempt.count(login, device_cookie) if count > config.getint('session', 'max_attempt', default=5): LoginAttempt.add(login, device_cookie) raise RateLimitException() Transaction().atexit(time.sleep, random.randint(0, 2**count - 1)) for methods in config.get('session', 'authentications', default='password').split(','): user_ids = set() for method in methods.split('+'): try: func = getattr(cls, '_login_%s' % method) except AttributeError: logger.info('Missing login method: %s', method) break user_ids.add(func(login, parameters)) if len(user_ids) != 1 or not all(user_ids): break if len(user_ids) == 1 and all(user_ids): LoginAttempt.remove(login, device_cookie) return user_ids.pop() LoginAttempt.add(login, device_cookie) @classmethod def _login_password(cls, login, parameters): if 'password' not in parameters: msg = gettext('res.msg_user_password', login=login) raise LoginException('password', msg, type='password') user_id, password_hash, password_reset = cls._get_login(login) if user_id and password_hash: password = parameters['password'] valid, new_hash = cls.check_password(password, password_hash) if valid: if new_hash: logger.info("Update password hash for %s", user_id) with Transaction().new_transaction() as transaction: with transaction.set_user(0): cls.write([cls(user_id)], { 'password_hash': new_hash, }) return user_id elif user_id and password_reset: if password_reset == parameters['password']: return user_id @classmethod def hash_password(cls, password): '''Hash given password in the form <hash_method>$<password>$<salt>...''' if not password: return None return CRYPT_CONTEXT.hash(password) @classmethod def check_password(cls, password, hash_): if not hash_: return False try: return CRYPT_CONTEXT.verify_and_update(password, hash_) except ValueError: hash_method = hash_.split('$', 1)[0] warnings.warn("Use deprecated hash method %s" % hash_method, DeprecationWarning) valid = getattr(cls, 'check_' + hash_method)(password, hash_) if valid: new_hash = CRYPT_CONTEXT.hash(password) else: new_hash = None return valid, new_hash @classmethod def hash_sha1(cls, password): salt = gen_password() salted_password = password + salt if isinstance(salted_password, str): salted_password = salted_password.encode('utf-8') hash_ = hashlib.sha1(salted_password).hexdigest() return '$'.join(['sha1', hash_, salt]) @classmethod def check_sha1(cls, password, hash_): if isinstance(password, str): password = password.encode('utf-8') hash_method, hash_, salt = hash_.split('$', 2) salt = salt or '' if isinstance(salt, str): salt = salt.encode('utf-8') assert hash_method == 'sha1' return hash_ == hashlib.sha1(password + salt).hexdigest() @classmethod def hash_bcrypt(cls, password): if isinstance(password, str): password = password.encode('utf-8') hash_ = bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8') return '$'.join(['bcrypt', hash_]) @classmethod def check_bcrypt(cls, password, hash_): if isinstance(password, str): password = password.encode('utf-8') hash_method, hash_ = hash_.split('$', 1) if isinstance(hash_, str): hash_ = hash_.encode('utf-8') assert hash_method == 'bcrypt' return hash_ == bcrypt.hashpw(password, hash_)
class Cache(ModelSQL): "Cache" __name__ = 'ir.cache' name = fields.Char('Name', required=True) timestamp = fields.Timestamp("Timestamp")
class User(ModelSQL, ModelView): 'Web User' __name__ = 'web.user' _rec_name = 'email' email = fields.Char('E-mail', required=True, select=True) email_valid = fields.Boolean('E-mail Valid') email_token = fields.Char('E-mail Token', select=True) password_hash = fields.Char('Password Hash') password = fields.Function(fields.Char('Password'), 'get_password', setter='set_password') reset_password_token = fields.Char('Reset Password Token', select=True) reset_password_token_expire = fields.Timestamp( 'Reset Password Token Expire') active = fields.Boolean('Active') party = fields.Many2One('party.party', 'Party') @classmethod def __setup__(cls): super(User, cls).__setup__() table = cls.__table__() cls._sql_constraints += [ ('email_unique', Unique(table, table.email), 'E-mail must be unique'), ] cls._buttons.update({ 'validate_email': { 'readonly': Eval('email_valid', False), }, 'reset_password': { 'readonly': ~Eval('email_valid', False), }, }) @staticmethod def default_active(): return True @classmethod def default_email_valid(cls): return False def get_password(self, name): return 'x' * 10 @classmethod def set_password(cls, users, name, value): pool = Pool() User = pool.get('res.user') if value == 'x' * 10: return if Transaction().user and value: User.validate_password(value, users) to_write = [] for user in users: to_write.extend([[user], { 'password_hash': cls.hash_password(value), }]) cls.write(*to_write) @classmethod def _format_email(cls, users): for user in users: email = user.email.lower() if email != user.email: user.email = email cls.save(users) @classmethod def create(cls, vlist): users = super(User, cls).create(vlist) cls._format_email(users) return users @classmethod def write(cls, *args): super(User, cls).write(*args) users = sum(args[0:None:2], []) cls._format_email(users) @classmethod def authenticate(cls, email, password): pool = Pool() Attempt = pool.get('web.user.authenticate.attempt') email = email.lower() # Prevent brute force attack Transaction().atexit(time.sleep, 2**Attempt.count(email) - 1) users = cls.search([('email', '=', email)]) if users: user, = users if cls.check_password(password, user.password_hash): Attempt.remove(email) return user Attempt.add(email) @staticmethod def hash_method(): return 'bcrypt' if bcrypt else 'sha1' @classmethod def hash_password(cls, password): '''Hash given password in the form <hash_method>$<password>$<salt>...''' if not password: return '' return getattr(cls, 'hash_' + cls.hash_method())(password) @classmethod def check_password(cls, password, hash_): if not hash_: return False hash_method = hash_.split('$', 1)[0] return getattr(cls, 'check_' + hash_method)(password, hash_) @classmethod def hash_sha1(cls, password): salt = ''.join(random.sample(string.ascii_letters + string.digits, 8)) salted_password = password + salt if isinstance(salted_password, unicode): salted_password = salted_password.encode('utf-8') hash_ = hashlib.sha1(salted_password).hexdigest() return '$'.join(['sha1', hash_, salt]) @classmethod def check_sha1(cls, password, hash_): if isinstance(password, unicode): password = password.encode('utf-8') hash_method, hash_, salt = hash_.split('$', 2) salt = salt or '' if isinstance(salt, unicode): salt = salt.encode('utf-8') assert hash_method == 'sha1' return hash_ == hashlib.sha1(password + salt).hexdigest() @classmethod def hash_bcrypt(cls, password): if isinstance(password, unicode): password = password.encode('utf-8') hash_ = bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8') return '$'.join(['bcrypt', hash_]) @classmethod def check_bcrypt(cls, password, hash_): if isinstance(password, unicode): password = password.encode('utf-8') hash_method, hash_ = hash_.split('$', 1) if isinstance(hash_, unicode): hash_ = hash_.encode('utf-8') assert hash_method == 'bcrypt' return hash_ == bcrypt.hashpw(password, hash_) def new_session(self): pool = Pool() Session = pool.get('web.user.session') return Session.add(self) @classmethod def get_user(cls, session): pool = Pool() Session = pool.get('web.user.session') return Session.get_user(session) @classmethod @ModelView.button def validate_email(cls, users, from_=None): for user in users: user.set_email_token() cls.save(users) _send_email(from_, users, cls.get_email_validation) def set_email_token(self, nbytes=None): self.email_token = token_hex(nbytes) def get_email_validation(self): return get_email('web.user.email_validation', self, self.languages) def get_email_validation_url(self, url=None): if url is None: url = config.get('web', 'email_validation_url') return _add_params(url, token=self.email_token) @classmethod def validate_email_url(cls, url): parts = urlparse.urlsplit(url) tokens = filter(None, urlparse.parse_qs(parts.query).get('token', [None])) return cls.validate_email_token(tokens) @classmethod def validate_email_token(cls, tokens): users = cls.search([ ('email_token', 'in', tokens), ]) cls.write(users, { 'email_valid': True, 'email_token': None, }) return bool(users) @classmethod @ModelView.button def reset_password(cls, users, from_=None): now = datetime.datetime.now() # Prevent abusive reset def reset(user): return not (user.reset_password_token_expire and user.reset_password_token_expire > now) users = filter(reset, users) for user in users: user.set_reset_password_token() cls.save(users) _send_email(from_, users, cls.get_email_reset_password) def set_reset_password_token(self, nbytes=None): self.reset_password_token = token_hex(nbytes) self.reset_password_token_expire = ( datetime.datetime.now() + datetime.timedelta(seconds=config.getint( 'session', 'web_timeout_reset', default=24 * 60 * 60))) def clear_reset_password_token(self): self.reset_password_token = None self.reset_password_token_expire = None def get_email_reset_password(self): return get_email('web.user.email_reset_password', self, self.languages) def get_email_reset_password_url(self, url=None): if url is None: url = config.get('web', 'reset_password_url') return _add_params(url, token=self.reset_password_token, email=self.email) @classmethod def set_password_url(cls, url, password): parts = urlparse.urlsplit(url) query = urlparse.parse_qs(parts.query) email = query.get('email', [None])[0] token = query.get('token', [None])[0] return cls.set_password_token(email, token, password) @classmethod def set_password_token(cls, email, token, password): pool = Pool() Attempt = pool.get('web.user.authenticate.attempt') email = email.lower() # Prevent brute force attack Transaction().atexit(time.sleep, 2**Attempt.count(email) - 1) users = cls.search([ ('email', '=', email), ]) if users: user, = users if user.reset_password_token == token: now = datetime.datetime.now() expire = user.reset_password_token_expire user.clear_reset_password_token() if expire > now: user.password = password user.save() Attempt.remove(email) return True Attempt.add(email) return False @property def languages(self): pool = Pool() Language = pool.get('ir.lang') if self.party and self.party.lang: languages = [self.party.lang] else: languages = Language.search([ ('code', '=', Transaction().language), ]) return languages
class Invoice(metaclass=PoolMeta): __name__ = 'account.invoice' numbered_at = fields.Timestamp("Numbered At") history_datetime = fields.Function(fields.Timestamp("History DateTime"), 'get_history_datetime') @classmethod def __register__(cls, module_name): super().__register__(module_name) table_h = cls.__table_handler__(module_name) table = cls.__table__() cursor = Transaction().connection.cursor() # Migration from 5.2: rename open_date into numbered_at if table_h.column_exist('open_date'): cursor.execute( *table.update([table.numbered_at], [table.open_date])) table_h.drop_column('open_date') @classmethod def __setup__(cls): super(Invoice, cls).__setup__() cls._check_modify_exclude.add('numbered_at') cls.party.datetime_field = 'history_datetime' cls.invoice_address.datetime_field = 'history_datetime' cls.payment_term.datetime_field = 'history_datetime' @classmethod def get_history_datetime(cls, invoices, name): pool = Pool() Party = pool.get('party.party') Address = pool.get('party.address') PaymentTerm = pool.get('account.invoice.payment_term') table = cls.__table__() party = Party.__table__() address = Address.__table__() payment_term = PaymentTerm.__table__() cursor = Transaction().connection.cursor() invoice_ids = [i.id for i in invoices] datetimes = dict.fromkeys(invoice_ids) for ids in grouped_slice(invoice_ids): cursor.execute( *table.join(party, condition=table.party == party.id).join( address, condition=table.invoice_address == address.id ).join(payment_term, 'LEFT', condition=table.payment_term == payment_term.id). select(table.id, Greatest(table.numbered_at, party.create_date, address.create_date, payment_term.create_date), where=reduce_ids(table.id, ids) & (table.numbered_at != Null))) datetimes.update(cursor) return datetimes @classmethod def set_number(cls, invoices): numbered = [i for i in invoices if not i.number or not i.numbered_at] super(Invoice, cls).set_number(invoices) if numbered: cls.write(numbered, { 'numbered_at': CurrentTimestamp(), }) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, invoices): super(Invoice, cls).draft(invoices) cls.write(invoices, { 'numbered_at': None, }) @classmethod def copy(cls, invoices, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('numbered_at', None) return super(Invoice, cls).copy(invoices, default=default)