mirror of
https://github.com/bringout/oca-server-auth.git
synced 2026-04-19 00:52:03 +02:00
Initial commit: OCA Server Auth packages (29 packages)
This commit is contained in:
commit
3ed80311c4
1325 changed files with 127292 additions and 0 deletions
19
odoo-bringout-oca-server-auth-vault/vault/models/__init__.py
Normal file
19
odoo-bringout-oca-server-auth-vault/vault/models/__init__.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
165
odoo-bringout-oca-server-auth-vault/vault/models/vault.py
Normal file
165
odoo-bringout-oca-server-auth-vault/vault/models/vault.py
Normal 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},
|
||||
}
|
||||
215
odoo-bringout-oca-server-auth-vault/vault/models/vault_entry.py
Normal file
215
odoo-bringout-oca-server-auth-vault/vault/models/vault_entry.py
Normal 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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
114
odoo-bringout-oca-server-auth-vault/vault/models/vault_inbox.py
Normal file
114
odoo-bringout-oca-server-auth-vault/vault/models/vault_inbox.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
110
odoo-bringout-oca-server-auth-vault/vault/models/vault_right.py
Normal file
110
odoo-bringout-oca-server-auth-vault/vault/models/vault_right.py
Normal 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()
|
||||
|
|
@ -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!")),
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue