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,9 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import (
vault_export_wizard,
vault_import_wizard,
vault_send_wizard,
vault_store_wizard,
)

View file

@ -0,0 +1,71 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import json
import logging
from datetime import datetime
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
class ExportWizard(models.TransientModel):
_name = "vault.export.wizard"
_description = _("Export wizard for vaults")
vault_id = fields.Many2one("vault", "Vault")
entry_id = fields.Many2one(
"vault.entry", "Entries", domain="[('vault_id', '=', vault_id)]"
)
master_key = fields.Char(related="vault_id.master_key")
name = fields.Char(default=lambda self: self._default_name())
content = fields.Binary("Download", attachment=False)
include_childs = fields.Boolean(default=True)
@api.onchange("vault_id", "entry_id")
def _onchange_content(self):
for rec in self.with_context(skip_log=True):
rec.content = self._export_content(
rec.vault_id,
rec.entry_id,
rec.include_childs,
)
def _default_name(self):
return datetime.now().strftime("database-%Y%m%d-%H%M.json")
def _export_content(self, vault=None, entry=None, include_childs=False):
if entry:
entries = entry
elif vault:
entries = vault.entry_ids.filtered_domain([("parent_id", "=", False)])
else:
return json.dumps([])
data = [self._export_entry(x, include_childs) for x in entries]
return json.dumps(data)
@api.model
def _export_field(self, rec):
def ensure_string(x):
return x.decode() if isinstance(x, bytes) else x
return {f: ensure_string(rec[f]) for f in ["name", "iv", "value"]}
@api.model
def _export_entry(self, entry, include_childs=False):
if include_childs:
childs = [self._export_entry(x, include_childs) for x in entry.child_ids]
else:
childs = []
return {
"uuid": entry.uuid,
"name": entry.name,
"note": entry.note,
"url": entry.url,
"fields": entry.field_ids.mapped(self._export_field),
"files": entry.file_ids.mapped(self._export_field),
"childs": childs,
}

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_vault_export_wizard" model="ir.ui.view">
<field name="model">vault.export.wizard</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="vault_id" invisible="1" />
<field name="entry_id" invisible="1" />
<field name="master_key" invisible="1" />
<field name="name" invisible="1" />
<group>
<field
name="content"
widget="vault_export_file"
filename="name"
readonly="1"
/>
<field name="include_childs" />
</group>
</sheet>
<footer>
<button type="special" string="Close" special="cancel" />
</footer>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,134 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import json
import logging
from uuid import uuid4
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class ImportWizardPath(models.TransientModel):
_name = "vault.import.wizard.path"
_description = _("Import wizard path for vaults")
name = fields.Char(required=True)
uuid = fields.Char(required=True)
class ImportWizard(models.TransientModel):
_name = "vault.import.wizard"
_description = _("Import wizard for vaults")
vault_id = fields.Many2one("vault", "Vault")
parent_id = fields.Many2one(
"vault.entry",
"Parent Entry",
domain="[('vault_id', '=', vault_id)]",
)
master_key = fields.Char(related="vault_id.master_key")
name = fields.Char()
content = fields.Binary("Database", attachment=False)
crypted_content = fields.Char()
uuid = fields.Char(default=lambda self: uuid4())
path = fields.Many2one(
"vault.import.wizard.path",
"Path to import",
default="",
domain="[('uuid', '=', uuid)]",
)
@api.onchange("crypted_content", "content")
def _onchange_content(self):
for rec in self:
if rec.crypted_content:
for entry in json.loads(rec.crypted_content or []):
rec._create_path(entry)
def _create_path(self, entry, path=None):
self.ensure_one()
p = f"{path} / {entry['name']}" if path else entry["name"]
if "name" in entry:
self.env["vault.import.wizard.path"].create({"uuid": self.uuid, "name": p})
for child in entry.get("childs", []):
self._create_path(child, p)
def _import_field(self, entry, model, data):
if not data:
return
# Only copy specific fields
vals = {f: data[f] for f in ["name", "iv", "value"]}
# Update already existing records
domain = [("entry_id", "=", entry.id), ("name", "=", data["name"])]
rec = model.search(domain)
if rec:
rec.write(vals)
else:
rec.create({"entry_id": entry.id, **vals})
def _import_entry(self, entry, parent=None, path=None):
p = f"{path} / {entry['name']}" if path else entry["name"]
result = self.env["vault.entry"]
if p.startswith(self.path.name or ""):
if not parent:
parent = self.env["vault.entry"]
# Update existing records if already imported
rec = self.env["vault.entry"]
if entry.get("uuid"):
domain = [
("uuid", "=", entry["uuid"]),
("vault_id", "=", self.vault_id.id),
]
rec = rec.search(domain, limit=1)
# If record not found create a new one
vals = {f: entry.get(f) for f in ["name", "note", "url", "uuid"]}
if not rec:
rec = rec.create(
{"vault_id": self.vault_id.id, "parent_id": parent.id, **vals}
)
else:
rec.write({"parent_id": parent.id, **vals})
# Create/update the entry fields
for field in entry.get("fields", []):
self._import_field(rec, self.env["vault.field"], field)
# Create/update the entry files
for file in entry.get("files", []):
self._import_field(rec, self.env["vault.file"], file)
result |= rec
else:
rec = None
# Create the sub-entries
for child in entry.get("childs", []):
result |= self._import_entry(child, rec, p)
return result
def action_import(self):
self.ensure_one()
try:
data = json.loads(self.crypted_content)
entries = self.env["vault.entry"]
for entry in data:
entries |= self.with_context(skip_log=True)._import_entry(
entry, self.parent_id
)
self.vault_id.log_entry(f"Imported entries from file {self.name}")
except Exception as e:
raise UserError(_("Invalid file to import from")) from e

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_vault_import_wizard" model="ir.ui.view">
<field name="model">vault.import.wizard</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="master_key" invisible="1" />
<field name="vault_id" invisible="1" />
<field name="crypted_content" invisible="1" />
<field name="name" invisible="1" />
<field name="uuid" invisible="1" />
<div>The files must end on one of the supported file type:</div>
<ul>
<li>Custom JSON format <b>.json</b></li>
<li>Keepass Database <b>.kdbx</b></li>
</ul>
<group>
<field name="content" filename="name" />
<field
name="parent_id"
options="{'no_create_edit': True, 'no_open': True}"
/>
<field
name="path"
attrs="{'invisible': [('crypted_content', '=', False)]}"
options="{'no_create_edit': True, 'no_open': True}"
/>
</group>
</sheet>
<footer>
<button
type="object"
name="action_import"
string="Import"
class="oe_highlight"
attrs="{'invisible': [('crypted_content', '=', False)]}"
/>
<button
type="object"
name="action_import"
string="Import"
attrs="{'invisible': [('crypted_content', '!=', False)]}"
/>
or
<button type="special" string="Cancel" special="cancel" />
</footer>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,56 @@
# © 2021 Florian Kantelberg - initOS GmbH
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import _, fields, models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class VaultSendWizard(models.TransientModel):
_name = "vault.send.wizard"
_description = _("Wizard to send another user a secret")
user_id = fields.Many2one(
"res.users",
"User",
required=True,
domain=[("keys", "!=", False), ("inbox_enabled", "=", True)],
)
name = fields.Char(required=True)
public = fields.Char(related="user_id.active_key.public")
iv = fields.Char(required=True)
key_user = fields.Char(required=True)
key = fields.Char(required=True)
secret = fields.Char()
secret_file = fields.Char()
filename = fields.Char()
_sql_constraints = [
(
"value_check",
"CHECK(secret IS NOT NULL OR secret_file IS NOT NULL)",
_("No value found"),
),
]
def action_send(self):
if not self.secret and not self.secret_file:
raise ValidationError(_("Neither a secret nor file was given"))
self.ensure_one()
self.env["vault.inbox"].sudo().create(
{
"name": self.name,
"accesses": 0,
"secret": self.secret,
"secret_file": self.secret_file,
"iv": self.iv,
"key": self.key_user,
"user_id": self.user_id.id,
"filename": self.filename,
"log_ids": [(0, 0, {"name": _("Created by %s") % self.user_id.name})],
}
)

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_vault_send_wizard" model="ir.ui.view">
<field name="model">vault.send.wizard</field>
<field name="arch" type="xml">
<form>
<sheet>
<div>
You can only send the secret to the user who has generated a key-pair.
If an user is not showing please ask him to generate these.
</div>
<group>
<field name="iv" invisible="1" />
<field name="key" invisible="1" />
<field name="key_user" invisible="1" />
<field name="secret" invisible="1" />
<field name="public" invisible="1" />
<field
name="user_id"
options="{'no_open': true, 'no_create_edit': true, 'no_quick_create': true}"
/>
<field name="name" />
</group>
</sheet>
<footer>
<button
type="object"
name="action_send"
string="Send"
class="oe_highlight"
/>
or
<button type="special" string="Cancel" special="cancel" />
</footer>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,47 @@
# © 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 VaultStoreWizard(models.TransientModel):
_name = "vault.store.wizard"
_description = _("Wizard store a shared secret in a vault")
vault_id = fields.Many2one("vault", "Vault", required=True)
entry_id = fields.Many2one(
"vault.entry",
"Entry",
domain="[('vault_id', '=', vault_id)]",
required=True,
)
model = fields.Char(required=True)
master_key = fields.Char(compute="_compute_master_key", store=False)
name = fields.Char(required=True)
iv = fields.Char(required=True)
key = fields.Char(required=True)
secret = fields.Char(required=True)
secret_temporary = fields.Char(required=True)
@api.depends("entry_id", "vault_id")
def _compute_master_key(self):
for rec in self:
rec.master_key = rec.vault_id.master_key
def action_store(self):
self.ensure_one()
try:
self.env[self.model].create(
{
"entry_id": self.entry_id.id,
"name": self.name,
"iv": self.iv,
"value": self.secret,
}
)
except Exception as e:
_logger.exception(e)

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_vault_store_wizard" model="ir.ui.view">
<field name="model">vault.store.wizard</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="iv" invisible="1" />
<field name="key" invisible="1" />
<field name="master_key" invisible="1" />
<field name="secret" invisible="1" />
<field name="secret_temporary" invisible="1" />
<field
name="vault_id"
options="{'no_create_edit': True, 'no_open': True, 'no_quick_create': true}"
/>
<field
name="entry_id"
attrs="{'invisible': [('vault_id', '=', False)]}"
options="{'no_create_edit': True, 'no_open': True, 'no_quick_create': true}"
/>
<field name="name" />
</group>
</sheet>
<footer>
<button
type="object"
name="action_store"
string="Store"
class="oe_highlight"
/>
or
<button type="special" string="Cancel" special="cancel" />
</footer>
</form>
</field>
</record>
</odoo>