Initial commit: OCA Server Auth packages (29 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:06 +02:00
commit 3ed80311c4
1325 changed files with 127292 additions and 0 deletions

View file

@ -0,0 +1,19 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import (
abstract_vault,
abstract_vault_field,
res_config_settings,
res_users,
res_users_key,
vault,
vault_entry,
vault_field,
vault_file,
vault_inbox,
vault_inbox_log,
vault_log,
vault_right,
vault_tag,
)

View file

@ -0,0 +1,77 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import _, api, models
from odoo.exceptions import AccessError
_logger = logging.getLogger(__name__)
class AbstractVault(models.AbstractModel):
"""Models must have the following fields:
`perm_user`: The permissions are computed for this user
`allowed_read`: The current user can read from the vault
`allowed_create`: The current user can read from the vault
`allowed_write`: The current user has write access to the vault
`allowed_share`: The current user can share the vault with other users
`allowed_delete`: The current user can delete the vault or entries of it
"""
_name = "vault.abstract"
_description = _("Abstract model to implement general access rights")
@api.model
def raise_access_error(self):
raise AccessError(
_(
"The requested operation can not be completed due to security "
"restrictions."
)
)
def check_access_rule(self, operation):
super().check_access_rule(operation)
if self.env.su:
return
# We have to recompute if the user of the environment changed
if self.env.user != self.mapped("perm_user"):
vault = self if self._name == "vault" else self.mapped("vault_id")
vault._compute_access()
# Shortcut for vault.right because only the share right is required
if self._name == "vault.right":
if not self.filtered("allowed_share"):
self.raise_access_error()
return
# Check the operation and matching permissions
if operation == "read" and not self.filtered("allowed_read"):
self.raise_access_error()
if operation == "create" and not self.filtered("allowed_create"):
self.raise_access_error()
if operation == "write" and not self.filtered("allowed_write"):
self.raise_access_error()
if operation == "unlink" and not self.filtered("allowed_delete"):
self.raise_access_error()
def _log_entry(self, msg, state):
raise NotImplementedError()
def log_entry(self, msg):
return self._log_entry(msg, None)
def log_info(self, msg):
return self._log_entry(msg, "info")
def log_warn(self, msg):
return self._log_entry(msg, "warn")
def log_error(self, msg):
return self._log_entry(msg, "error")

View file

@ -0,0 +1,57 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
class AbstractVaultField(models.AbstractModel):
_name = "vault.abstract.field"
_description = _("Abstract model to implement basic fields for encryption")
entry_id = fields.Many2one("vault.entry", ondelete="cascade", required=True)
entry_name = fields.Char(related="entry_id.complete_name")
vault_id = fields.Many2one(related="entry_id.vault_id")
master_key = fields.Char(compute="_compute_master_key", store=False)
perm_user = fields.Many2one(related="vault_id.perm_user", store=False)
allowed_read = fields.Boolean(related="vault_id.allowed_read", store=False)
allowed_create = fields.Boolean(related="vault_id.allowed_create", store=False)
allowed_write = fields.Boolean(related="vault_id.allowed_write", store=False)
allowed_share = fields.Boolean(related="vault_id.allowed_share", store=False)
allowed_delete = fields.Boolean(related="vault_id.allowed_delete", store=False)
name = fields.Char(required=True)
iv = fields.Char()
@api.depends("entry_id.vault_id.master_key")
def _compute_master_key(self):
for rec in self:
rec.master_key = rec.vault_id.master_key
def log_change(self, action):
if self.env.context.get("vault_skip_log"):
return
for rec in self:
rec.entry_id.log_info(
f"{action} value {rec.name} of {rec.entry_id.complete_name} "
f"by {self.env.user.display_name}"
)
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
res.log_change("Created")
return res
def unlink(self):
self.log_change("Deleted")
return super().unlink()
def write(self, values):
self.log_change("Changed")
return super().write(values)

View file

@ -0,0 +1,16 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
module_vault_share = fields.Boolean()
group_vault_export = fields.Boolean(
"Export Vaults", implied_group="vault.group_vault_export"
)
group_vault_import = fields.Boolean(
"Import Vaults", implied_group="vault.group_vault_import"
)

View file

@ -0,0 +1,78 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from uuid import uuid4
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class ResUsers(models.Model):
_inherit = "res.users"
active_key = fields.Many2one(
"res.users.key",
compute="_compute_active_key",
store=False,
)
keys = fields.One2many("res.users.key", "user_id", readonly=True)
vault_right_ids = fields.One2many("vault.right", "user_id", readonly=True)
inbox_ids = fields.One2many("vault.inbox", "user_id")
inbox_enabled = fields.Boolean(default=True)
inbox_link = fields.Char(compute="_compute_inbox_link", readonly=True, store=False)
inbox_token = fields.Char(default=lambda self: uuid4(), readonly=True)
@api.depends("keys", "keys.current")
def _compute_active_key(self):
for rec in self:
keys = rec.sudo().keys.filtered("current")
rec.active_key = keys[0] if keys else None
@api.depends("inbox_token")
def _compute_inbox_link(self):
base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
for rec in self:
rec.inbox_link = f"{base_url}/vault/inbox/{rec.inbox_token}"
@api.model
def action_get_vault(self):
action = self.sudo().env.ref("vault.action_res_users_keys")
result = action.read()[0]
result["res_id"] = self.env.uid
return result
def action_new_inbox_token(self):
self.ensure_one()
self.sudo().inbox_token = uuid4()
return self.action_get_vault()
def action_invalidate_key(self):
"""Disable the current key and remove all accesses to the vaults"""
self.ensure_one()
self.keys.write({"current": False})
self.vault_right_ids.sudo().unlink()
self.inbox_ids.unlink()
self.env["vault"].search([])._compute_access()
return self.action_get_vault()
@api.model
def find_user_of_inbox(self, token):
return self.search([("inbox_token", "=", token), ("inbox_enabled", "=", True)])
def get_vault_keys(self):
self.ensure_one()
if not self.active_key:
return {}
return {
"iterations": self.active_key.iterations,
"iv": self.active_key.iv,
"private": self.active_key.private,
"public": self.active_key.public,
"salt": self.active_key.salt,
"uuid": self.active_key.uuid,
"version": self.active_key.version,
}

View file

@ -0,0 +1,82 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
import re
from hashlib import sha256
from uuid import uuid4
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class ResUsersKey(models.Model):
_name = "res.users.key"
_description = _("User data of a vault")
_rec_name = "fingerprint"
_order = "create_date DESC"
user_id = fields.Many2one("res.users", required=True)
uuid = fields.Char(default=lambda self: uuid4(), required=True, readonly=True)
current = fields.Boolean(default=True, readonly=True)
fingerprint = fields.Char(compute="_compute_fingerprint", store=True)
public = fields.Char(required=True, readonly=True)
salt = fields.Char(required=True, readonly=True)
iv = fields.Char(required=True, readonly=True)
iterations = fields.Integer(required=True, readonly=True)
version = fields.Integer(readonly=True)
# Encrypted with master password of user
private = fields.Char(required=True, readonly=True)
@api.depends("public")
def _compute_fingerprint(self):
for rec in self:
if rec.public:
hashed = sha256(rec.public.encode()).hexdigest()
rec.fingerprint = ":".join(re.findall(r".{2}", hashed))
else:
rec.fingerprint = False
def _prepare_values(self, iterations, iv, private, public, salt, version):
return {
"iterations": iterations,
"iv": iv,
"private": private,
"public": public,
"salt": salt,
"user_id": self.env.uid,
"current": True,
"version": version,
}
def store(self, iterations, iv, private, public, salt, version):
if not all(isinstance(x, str) and x for x in [public, private, iv, salt]):
raise ValidationError(_("Invalid parameter"))
if not isinstance(iterations, int) or iterations < 4000:
raise ValidationError(_("Invalid parameter"))
if not isinstance(version, int):
raise ValidationError(_("Invalid parameter"))
domain = [
("user_id", "=", self.env.uid),
("private", "=", private),
]
key = self.search(domain)
if not key:
# Disable all current keys
self.env.user.keys.write({"current": False})
rec = self.create(
self._prepare_values(iterations, iv, private, public, salt, version)
)
return rec.uuid
return False
def extract_public_key(self, user):
user = self.sudo().search([("user_id", "=", user), ("current", "=", True)])
return user.public or None

View file

@ -0,0 +1,165 @@
# © 2021-2024 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from uuid import uuid4
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
class Vault(models.Model):
_name = "vault"
_description = _("Vault")
_inherit = ["vault.abstract"]
_order = "name"
user_id = fields.Many2one(
"res.users",
"Owner",
readonly=True,
default=lambda self: self.env.user,
required=True,
)
right_ids = fields.One2many(
"vault.right",
"vault_id",
"Rights",
default=lambda self: self._get_default_rights(),
)
entry_ids = fields.One2many("vault.entry", "vault_id", "Entries")
field_ids = fields.One2many("vault.field", "vault_id", "Fields")
file_ids = fields.One2many("vault.file", "vault_id", "Files")
log_ids = fields.One2many("vault.log", "vault_id", "Log", readonly=True)
reencrypt_required = fields.Boolean(default=False)
# Access control
perm_user = fields.Many2one("res.users", compute="_compute_access", store=False)
allowed_read = fields.Boolean(compute="_compute_access", store=False)
allowed_create = fields.Boolean(compute="_compute_access", store=False)
allowed_share = fields.Boolean(compute="_compute_access", store=False)
allowed_write = fields.Boolean(compute="_compute_access", store=False)
allowed_delete = fields.Boolean(compute="_compute_access", store=False)
master_key = fields.Char(
compute="_compute_master_key",
inverse="_inverse_master_key",
store=False,
)
uuid = fields.Char(default=lambda self: uuid4(), required=True, readonly=True)
name = fields.Char(required=True)
note = fields.Text()
_sql_constraints = [
("uuid_uniq", "UNIQUE(uuid)", _("The UUID must be unique.")),
]
@api.depends("right_ids.user_id")
def _compute_access(self):
user = self.env.user
for rec in self.sudo():
rec.perm_user = user.id
if user == rec.user_id:
rec.write(
{
"allowed_create": True,
"allowed_share": True,
"allowed_write": True,
"allowed_delete": True,
"allowed_read": True,
}
)
continue
rights = rec.right_ids
rec.allowed_read = user in rights.mapped("user_id")
rec.allowed_create = user in rights.filtered("perm_create").mapped(
"user_id"
)
rec.allowed_share = user in rights.filtered("perm_share").mapped("user_id")
rec.allowed_write = user in rights.filtered("perm_write").mapped("user_id")
rec.allowed_delete = user in rights.filtered("perm_delete").mapped(
"user_id"
)
@api.depends("right_ids.key")
def _compute_master_key(self):
domain = [("user_id", "=", self.env.uid)]
for rec in self:
rights = rec.right_ids.filtered_domain(domain)
rec.master_key = rights[0].key if rights else False
def _inverse_master_key(self):
domain = [("user_id", "=", self.env.uid)]
for rec in self:
rights = rec.right_ids.filtered_domain(domain)
if rights and not rights.key:
rights.key = rec.master_key
def _get_default_rights(self):
return [
(
0,
0,
{
"user_id": self.env.uid,
"perm_create": True,
"perm_write": True,
"perm_delete": True,
"perm_share": True,
},
)
]
def _log_entry(self, msg, state):
self.ensure_one()
return (
self.env["vault.log"]
.sudo()
.create(
{
"vault_id": self.id,
"user_id": self.env.uid,
"message": msg,
"state": state,
}
)
)
def share_public_keys(self):
self.ensure_one()
result = []
for right in self.right_ids:
result.append({"user": right.user_id.id, "public": right.public_key})
return result
def action_open_import_wizard(self):
self.ensure_one()
wizard = self.env.ref("vault.view_vault_import_wizard")
return {
"name": _("Import from file"),
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "vault.import.wizard",
"views": [(wizard.id, "form")],
"view_id": wizard.id,
"target": "new",
"context": {"default_vault_id": self.id},
}
def action_open_export_wizard(self):
self.ensure_one()
wizard = self.env.ref("vault.view_vault_export_wizard")
return {
"name": _("Export to file"),
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "vault.export.wizard",
"views": [(wizard.id, "form")],
"view_id": wizard.id,
"target": "new",
"context": {"default_vault_id": self.id},
}

View file

@ -0,0 +1,215 @@
# © 2021 Florian Kantelberg - initOS GmbH
# Copyright 2022 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from datetime import datetime
from uuid import uuid4
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class VaultEntry(models.Model):
_name = "vault.entry"
_description = _("Entry inside a vault")
_inherit = ["vault.abstract"]
_order = "complete_name"
_rec_name = "complete_name"
parent_id = fields.Many2one(
"vault.entry",
"Parent",
ondelete="cascade",
domain="[('vault_id', '=', vault_id)]",
)
child_ids = fields.One2many("vault.entry", "parent_id", "Child")
vault_id = fields.Many2one("vault", "Vault", ondelete="cascade", required=True)
user_id = fields.Many2one(related="vault_id.user_id")
field_ids = fields.One2many("vault.field", "entry_id", "Fields")
file_ids = fields.One2many("vault.file", "entry_id", "Files")
log_ids = fields.One2many("vault.log", "entry_id", "Log", readonly=True)
perm_user = fields.Many2one(related="vault_id.perm_user", store=False)
allowed_read = fields.Boolean(related="vault_id.allowed_read", store=False)
allowed_create = fields.Boolean(related="vault_id.allowed_create", store=False)
allowed_share = fields.Boolean(related="vault_id.allowed_share", store=False)
allowed_write = fields.Boolean(related="vault_id.allowed_write", store=False)
allowed_delete = fields.Boolean(related="vault_id.allowed_delete", store=False)
complete_name = fields.Char(
compute="_compute_complete_name",
store=True,
readonly=True,
recursive=True,
)
uuid = fields.Char(default=lambda self: uuid4(), required=True, copy=False)
name = fields.Char(required=True)
url = fields.Char()
note = fields.Text()
tags = fields.Many2many("vault.tag")
expire_date = fields.Datetime("Expires on", default=False)
expired = fields.Boolean(
compute="_compute_expired",
search="_search_expired",
store=False,
)
_sql_constraints = [
("vault_uuid_uniq", "UNIQUE(vault_id, uuid)", _("The UUID must be unique.")),
]
@api.constrains("parent_id")
def _check_parent_id(self):
if not self._check_recursion():
raise ValidationError(_("You can not create recursive entries."))
@api.depends("name", "parent_id.complete_name")
def _compute_complete_name(self):
for rec in self:
if rec.parent_id:
rec.complete_name = f"{rec.parent_id.complete_name} / {rec.name}"
else:
rec.complete_name = rec.name
@api.model
def search_panel_select_range(self, field_name, **kwargs):
"""We add the following contexts related to searchpanel:
- entry_short_name: Show just the name instead of full path.
- from_search_panel: It will be used to overwrite domain.
Remove the limit of records (default is 200).
"""
return super(
VaultEntry,
self.with_context(from_search_panel=True, entry_short_name=True),
).search_panel_select_range(field_name, **kwargs)
def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None):
"""Changes related to searchpanel:
- Add a domain to only show records with children.
"""
domain = domain if domain else []
if self.env.context.get("from_search_panel"):
domain += [("child_ids", "!=", False)]
return super().search_read(
domain=domain, fields=fields, offset=offset, limit=limit, order=order
)
def copy_data(self, default=None):
self.ensure_one()
if default is None:
default = {}
if "name" not in default:
default["name"] = _("%s (copy)", self.name)
if "field_ids" not in default:
default["field_ids"] = [
(0, 0, field.copy_data()[0]) for field in self.field_ids
]
if "file_ids" not in default:
default["file_ids"] = [
(0, 0, field.copy_data()[0]) for field in self.file_ids
]
return super().copy_data(default)
@api.depends("name", "complete_name")
def _compute_display_name(self):
if not self.env.context.get("entry_short_name", False):
return super()._compute_display_name()
for record in self:
record.display_name = record.name
@api.depends("expire_date")
def _compute_expired(self):
now = datetime.now()
for rec in self:
rec.expired = rec.expire_date and now > rec.expire_date
def _search_expired(self, operator, value):
if (operator not in ["=", "!="]) or (value not in [True, False]):
return []
if (operator, value) in [("=", True), ("!=", False)]:
return [("expire_date", "<", datetime.now())]
return ["|", ("expire_date", ">=", datetime.now()), ("expire_date", "=", False)]
def log_change(self, action):
if self.env.context.get("vault_skip_log"):
return
for rec in self:
rec.log_info(
_("%(action)s entry %(name)s by %(user)s")
% {
"action": action,
"name": rec.complete_name,
"user": rec.env.user.display_name,
}
)
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
res.log_change("Created")
return res
def unlink(self):
self.log_change("Deleted")
return super().unlink()
def _log_entry(self, msg, state):
self.ensure_one()
return (
self.env["vault.log"]
.sudo()
.create(
{
"vault_id": self.vault_id.id,
"entry_id": self.id,
"user_id": self.env.uid,
"message": msg,
"state": state,
}
)
)
def action_open_import_wizard(self):
self.ensure_one()
wizard = self.env.ref("vault.view_vault_import_wizard")
return {
"name": _("Import from file"),
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "vault.import.wizard",
"views": [(wizard.id, "form")],
"view_id": wizard.id,
"target": "new",
"context": {
"default_vault_id": self.vault_id.id,
"default_parent_id": self.id,
},
}
def action_open_export_wizard(self):
self.ensure_one()
wizard = self.env.ref("vault.view_vault_export_wizard")
return {
"name": _("Export to file"),
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "vault.export.wizard",
"views": [(wizard.id, "form")],
"view_id": wizard.id,
"target": "new",
"context": {
"default_vault_id": self.vault_id.id,
"default_entry_id": self.id,
},
}

View file

@ -0,0 +1,17 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import _, fields, models
_logger = logging.getLogger(__name__)
class VaultField(models.Model):
_name = "vault.field"
_description = _("Field of a vault")
_order = "name"
_inherit = ["vault.abstract.field", "vault.abstract"]
value = fields.Char(required=True)

View file

@ -0,0 +1,25 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
class VaultFile(models.Model):
_name = "vault.file"
_description = _("File of a vault")
_order = "name"
_inherit = ["vault.abstract.field", "vault.abstract"]
value = fields.Binary(attachment=False)
@api.model
def search_read(self, *args, **kwargs):
if self.env.context.get("vault_reencrypt"):
return super(VaultFile, self.with_context(bin_size=False)).search_read(
*args, **kwargs
)
return super().search_read(*args, **kwargs)

View file

@ -0,0 +1,114 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from datetime import datetime, timedelta
from uuid import uuid4
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
class VaultInbox(models.Model):
_name = "vault.inbox"
_description = _("Vault share incoming secrets")
token = fields.Char(default=lambda self: uuid4(), readonly=True, copy=False)
inbox_link = fields.Char(
compute="_compute_inbox_link",
readonly=True,
help="Using this link you can write to the current inbox. If you want people "
"to create new inboxes you should give them your inbox link from your key "
"management.",
)
user_id = fields.Many2one(
"res.users",
"Vault",
required=True,
)
name = fields.Char(required=True)
secret = fields.Char(readonly=True)
filename = fields.Char()
secret_file = fields.Binary(attachment=False, readonly=True)
key = fields.Char(required=True)
iv = fields.Char(required=True)
accesses = fields.Integer(
"Access counter",
default=1,
help="If this is 0 the inbox can't be written using the link",
)
expiration = fields.Datetime(
default=lambda self: datetime.now() + timedelta(days=7),
help="If expired the inbox can't be written using the link",
)
log_ids = fields.One2many("vault.inbox.log", "inbox_id", "Log", readonly=True)
_sql_constraints = [
(
"value_check",
"CHECK(secret IS NOT NULL OR secret_file IS NOT NULL)",
_("No value found"),
),
]
@api.depends("token")
def _compute_inbox_link(self):
base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
for rec in self:
rec.inbox_link = f"{base_url}/vault/inbox/{rec.token}"
def read(self, *args, **kwargs):
# Always load the binary instead of the size
return super(VaultInbox, self.with_context(bin_size=False)).read(
*args, **kwargs
)
@api.model
def find_inbox(self, token):
return self.search([("token", "=", token)])
def store_in_inbox(
self,
name,
secret,
secret_file,
iv,
key,
user,
filename,
ip=None,
):
log_info = {"name": user.name, "ip": ip or "n/a"}
if len(self) == 0:
log = _("Created by %(name)s via %(ip)s") % log_info
return self.create(
{
"name": name,
"accesses": 0,
"iv": iv,
"key": key,
"secret": secret or None,
"secret_file": secret_file or None,
"filename": filename,
"user_id": user.id,
"log_ids": [(0, 0, {"name": log})],
}
)
self.ensure_one()
if self.accesses > 0 and datetime.now() < self.expiration:
log = _("Written by %(name)s via %(ip)s") % log_info
self.write(
{
"accesses": self.accesses - 1,
"iv": iv,
"key": key,
"secret": secret or None,
"secret_file": secret_file or None,
"filename": filename,
"log_ids": [(0, 0, {"name": log})],
}
)
return self

View file

@ -0,0 +1,22 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import _, fields, models
_logger = logging.getLogger(__name__)
class VaultInboxLog(models.Model):
_name = "vault.inbox.log"
_description = _("Vault inbox log")
_order = "create_date DESC"
inbox_id = fields.Many2one(
"vault.inbox",
ondelete="cascade",
readonly=True,
required=True,
)
name = fields.Char(readonly=True)

View file

@ -0,0 +1,46 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
class VaultLog(models.Model):
_name = "vault.log"
_description = _("Log entry of a vault")
_order = "create_date DESC"
_rec_name = "message"
vault_id = fields.Many2one(
"vault",
"Vault",
ondelete="cascade",
required=True,
readonly=True,
)
entry_id = fields.Many2one(
"vault.entry",
"Entry",
ondelete="cascade",
readonly=True,
)
user_id = fields.Many2one("res.users", "User", required=True, readonly=True)
state = fields.Selection(lambda self: self._get_log_state(), readonly=True)
message = fields.Char(readonly=True, required=True)
def _get_log_state(self):
return [
("info", _("Information")),
("warn", _("Warning")),
("error", _("Error")),
]
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
if not self.env.context.get("skip_log", False):
_logger.info("Vault log: %s", res.message)
return res

View file

@ -0,0 +1,110 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
class VaultRight(models.Model):
_name = "vault.right"
_description = _("Vault rights")
_inherit = ["vault.abstract"]
_order = "user_id"
vault_id = fields.Many2one(
"vault",
"Vault",
readonly=True,
required=True,
ondelete="cascade",
)
master_key = fields.Char(related="vault_id.master_key", readonly=True, store=False)
user_id = fields.Many2one(
"res.users",
"User",
domain=[("keys", "!=", False)],
required=True,
)
public_key = fields.Char(compute="_compute_public_key", readonly=True, store=False)
perm_create = fields.Boolean(
"Create",
default=lambda self: self._get_is_owner(),
help="Allow to create in the vault",
)
perm_write = fields.Boolean(
"Write",
default=lambda self: self._get_is_owner(),
help="Allow to write to the vault",
)
perm_share = fields.Boolean(
"Share",
default=lambda self: self._get_is_owner(),
help="Allow to share a vault with new users",
)
perm_delete = fields.Boolean(
"Delete",
default=lambda self: self._get_is_owner(),
help="Allow to delete a vault",
)
perm_user = fields.Many2one(related="vault_id.perm_user", store=False)
allowed_read = fields.Boolean(related="vault_id.allowed_read", store=False)
allowed_create = fields.Boolean(related="vault_id.allowed_create", store=False)
allowed_write = fields.Boolean(related="vault_id.allowed_write", store=False)
allowed_share = fields.Boolean(related="vault_id.allowed_share", store=False)
allowed_delete = fields.Boolean(related="vault_id.allowed_delete", store=False)
# Encrypted with the public key of the user
key = fields.Char()
_sql_constraints = (
("user_uniq", "UNIQUE(user_id, vault_id)", _("The user must be unique")),
)
def _get_is_owner(self):
return self.env.user == self.vault_id.user_id
@api.depends("user_id")
def _compute_public_key(self):
for rec in self:
rec.public_key = rec.user_id.active_key.public
def log_access(self):
for rec in self:
rights = ", ".join(
sorted(
["read"]
+ [
right
for right in ["create", "write", "share", "delete"]
if getattr(rec, f"perm_{right}", False)
]
)
)
rec.vault_id.log_info(
f"Grant access to user {rec.user_id.display_name}: {rights}"
)
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
if not res.allowed_share and not res.env.su:
self.raise_access_error()
res.log_access()
return res
def write(self, values):
res = super().write(values)
perms = ["perm_write", "perm_delete", "perm_share", "perm_create"]
if any(x in values for x in perms):
self.log_access()
return res
def unlink(self):
for rec in self:
rec.vault_id.log_info(f"Removed user {self.user_id.display_name}")
rec.vault_id.reencrypt_required = True
return super().unlink()

View file

@ -0,0 +1,16 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, fields, models
class VaultTag(models.Model):
_name = "vault.tag"
_description = _("Vault tag")
_order = "name"
name = fields.Char(required=True)
_sql_constraints = [
("name_uniq", "unique(name)", _("The tag must be unique!")),
]