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,121 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<t t-name="vault.Field.buttons.send" owl="1">
<button
t-if="sendButton"
t-on-click="_onSendValue"
class="btn btn-secondary btn-sm fa fa-share-alt o_vault_send"
title="Send the secret to an user"
aria-label="Send the secret to an user"
/>
</t>
<t t-name="vault.Field.buttons" owl="1">
<button
t-if="!state.decrypted &amp;&amp; showButton"
t-on-click="_onShowValue"
class="btn btn-secondary btn-sm fa fa-eye o_vault_show"
title="Show"
aria-label="Show"
/>
<button
t-elif="showButton"
t-on-click="_onShowValue"
class="btn btn-secondary btn-sm fa fa-eye-slash o_vault_show"
title="Hide"
aria-label="Hide"
/>
<button
t-if="copyButton"
t-on-click="_onCopyValue"
class="btn btn-secondary btn-sm fa fa-clipboard o_vault_clipboard"
title="Copy to clipboard"
aria-label="Copy to clipboard"
/>
<t t-call="vault.Field.buttons.send" />
</t>
<t t-name="vault.FieldVault" owl="1">
<div class="o_vault o_vault_error" t-if="!supported()">
<span>*******</span>
</div>
<div class="o_vault" t-elif="props.readonly">
<span class="o_vault_buttons">
<t t-call="vault.Field.buttons" />
</span>
<span t-esc="formattedValue" t-ref="span" />
</div>
<div class="o_vault" t-else="">
<span class="o_vault_buttons">
<button
t-if="generateButton"
t-on-click="_onGenerateValue"
class="btn btn-secondary btn-sm fa fa-lock o_vault_generate"
title="Generate"
aria-label="Generate"
/>
</span>
<input class="o_input" type="text" t-esc="formattedValue" t-ref="input" />
</div>
</t>
<t
t-name="vault.FileVault"
t-inherit="web.BinaryField"
t-inherit-mode="primary"
owl="1"
>
<xpath expr="//button[hasclass('o_clear_file_button')]" position="after">
<t t-call="vault.Field.buttons.send" />
</xpath>
</t>
<t t-name="vault.FieldVaultInbox" owl="1">
<div class="o_vault o_vault_error" t-if="!supported()">
<span>*******</span>
</div>
<div class="o_vault" t-elif="props.value">
<span class="o_vault_buttons">
<t t-call="vault.Field.buttons" />
<button
t-if="saveButton"
t-on-click="_onSaveValue"
class="btn btn-secondary btn-sm fa fa-save"
title="Save in a vault"
aria-label="Save in a vault"
/>
</span>
<span class="o_vault_inbox" t-esc="formattedValue" t-ref="span" />
</div>
</t>
<t t-name="vault.FileVaultInbox" owl="1">
<div class="o_vault o_vault_error" t-if="!supported()">
<span>*******</span>
</div>
<div class="o_vault" t-elif="props.value">
<span class="o_vault_buttons">
<button
t-if="saveButton"
t-on-click="_onSaveValue"
class="btn btn-secondary btn-sm fa fa-save"
title="Save in a vault"
aria-label="Save in a vault"
/>
</span>
<a class="o_form_uri" href="#" t-on-click.prevent="onFileDownload">
<span class="fa fa-download me-2" />
<t t-if="state.fileName" t-esc="state.fileName" />
</a>
</div>
</t>
<t t-name="vault.FileVaultExport" owl="1">
<a class="o_form_uri" href="#" t-on-click.prevent="onFileDownload">
<span class="fa fa-download me-2" />
<t t-if="state.fileName" t-esc="state.fileName" />
</a>
</t>
</templates>

View file

@ -0,0 +1,45 @@
/** @odoo-module alias=vault.export.file **/
// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import {BinaryField} from "@web/views/fields/binary/binary_field";
import Exporter from "vault.export";
import VaultMixin from "vault.mixin";
import {_lt} from "@web/core/l10n/translation";
import {downloadFile} from "@web/core/network/download";
import {registry} from "@web/core/registry";
import utils from "vault.utils";
export default class VaultExportFile extends VaultMixin(BinaryField) {
/**
* Call the exporter and download the finalized file
*/
async onFileDownload() {
if (!this.props.value) {
this.do_warn(
_lt("Save As..."),
_lt("The field is empty, there's nothing to save!")
);
} else if (utils.supported()) {
const exporter = new Exporter();
const content = JSON.stringify(
await exporter.export(
await this._getMasterKey(),
this.state.fileName,
this.props.value
)
);
const buffer = new ArrayBuffer(content.length);
const arr = new Uint8Array(buffer);
for (let i = 0; i < content.length; i++) arr[i] = content.charCodeAt(i);
const blob = new Blob([arr]);
await downloadFile(blob, this.state.fileName || "");
}
}
}
VaultExportFile.template = "vault.FileVaultExport";
registry.category("fields").add("vault_export_file", VaultExportFile);

View file

@ -0,0 +1,205 @@
/** @odoo-module alias=vault.field **/
// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import {Component, useEffect, useRef, useState} from "@odoo/owl";
import {useBus, useService} from "@web/core/utils/hooks";
import VaultMixin from "vault.mixin";
import {_lt} from "@web/core/l10n/translation";
import {getActiveHotkey} from "@web/core/hotkeys/hotkey_service";
import {registry} from "@web/core/registry";
import utils from "vault.utils";
export default class VaultField extends VaultMixin(Component) {
setup() {
super.setup();
this.action = useService("action");
this.input = useRef("input");
this.span = useRef("span");
this.state = useState({
decrypted: false,
decryptedValue: "",
isDirty: false,
lastSetValue: null,
});
const self = this;
useEffect(
(inputEl) => {
if (inputEl) {
const onInput = self.onInput.bind(self);
const onKeydown = self.onKeydown.bind(self);
inputEl.addEventListener("input", onInput);
inputEl.addEventListener("keydown", onKeydown);
return () => {
inputEl.removeEventListener("input", onInput);
inputEl.removeEventListener("keydown", onKeydown);
};
}
},
() => [self.input.el]
);
useEffect(() => {
const isInvalid = self.props.record
? self.props.record.isInvalid(self.props.name)
: false;
if (self.input.el && !self.state.isDirty && !isInvalid) {
Promise.resolve(self.getValue()).then((val) => {
if (!self.input.el) return;
if (val) self.input.el.value = val;
else if (val !== "")
self.props.record.setInvalidField(self.props.name);
});
self.state.lastSetValue = self.input.el.value;
}
});
useBus(self.env.bus, "RELATIONAL_MODEL:WILL_SAVE_URGENTLY", () =>
self.commitChanges(true)
);
useBus(self.env.bus, "RELATIONAL_MODEL:NEED_LOCAL_CHANGES", (ev) =>
ev.detail.proms.push(self.commitChanges())
);
useBus(self.env.bus, "RELATIONAL_MODEL:ENCRYPT_FIELDS", () => {
this.state.decrypted = false;
this.showValue();
});
}
/**
* Open a dialog to generate a new secret
*
* @param {Object} ev
*/
async _onGenerateValue(ev) {
ev.stopPropagation();
const password = await utils.generate_pass();
await this.storeValue(password);
}
/**
* Toggle between visible and invisible secret
*
* @param {Object} ev
*/
async _onShowValue(ev) {
ev.stopPropagation();
this.state.decrypted = !this.state.decrypted;
if (this.state.decrypted) {
this.state.decryptedValue = await this._decrypt(this.props.value);
} else {
this.state.decryptedValue = "";
}
await this.showValue();
}
/**
* Copy the decrypted secret to the clipboard
*
* @param {Object} ev
*/
async _onCopyValue(ev) {
ev.stopPropagation();
const value = await this._decrypt(this.props.value);
await navigator.clipboard.writeText(value);
}
/**
* Send the secret to an inbox of an user
*
* @param {Object} ev
*/
async _onSendValue(ev) {
ev.stopPropagation();
await this.sendValue(this.props.value, "");
}
/**
* Get the decrypted value or a placeholder
*
* @returns the decrypted value or a placeholder
*/
get formattedValue() {
if (!this.props.value) return "";
if (this.state.decrypted) return this.state.decryptedValue || "*******";
return "*******";
}
/**
* Decrypt the value of the field
*
* @returns decrypted value
*/
async getValue() {
return await this._decrypt(this.props.value);
}
/**
* Update the value shown
*/
async showValue() {
this.span.el.innerHTML = this.formattedValue;
}
/**
* Handle input event and set the state to dirty
*
* @param {Object} ev
*/
onInput(ev) {
ev.stopPropagation();
this.state.isDirty = ev.target.value !== this.lastSetValue;
if (this.props.setDirty) this.props.setDirty(this.state.isDirty);
}
/**
* Commit the changes of the input field to the record
*
* @param {Boolean} urgent
*/
async commitChanges(urgent) {
if (!this.input.el) return;
this.state.isDirty = this.input.el.value !== this.lastSetValue;
if (this.state.isDirty || urgent) {
this.state.isDirty = false;
const val = this.input.el.value || false;
if (val !== (this.state.lastSetValue || false)) {
this.state.lastSetValue = this.input.el.value;
this.state.decryptedValue = this.input.el.value;
await this.storeValue(val);
this.props.setDirty(this.state.isDirty);
}
}
}
/**
* Handle keyboard events and trigger changes
*
* @param {Object} ev
*/
onKeydown(ev) {
ev.stopPropagation();
const hotkey = getActiveHotkey(ev);
if (["enter", "tab", "shift+tab"].includes(hotkey)) this.commitChanges(false);
}
}
VaultField.displayName = _lt("Vault Field");
VaultField.supportedTypes = ["char"];
VaultField.template = "vault.FieldVault";
registry.category("fields").add("vault_field", VaultField);

View file

@ -0,0 +1,61 @@
/** @odoo-module alias=vault.file **/
// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import {BinaryField} from "@web/views/fields/binary/binary_field";
import VaultMixin from "vault.mixin";
import {_lt} from "@web/core/l10n/translation";
import {downloadFile} from "@web/core/network/download";
import {registry} from "@web/core/registry";
import {useService} from "@web/core/utils/hooks";
import utils from "vault.utils";
export default class VaultFile extends VaultMixin(BinaryField) {
setup() {
super.setup();
this.action = useService("action");
}
async update({data, name}) {
const encrypted = await this._encrypt(data);
return await super.update({data: encrypted, name: name});
}
/**
* Send the secret to an inbox of an user
*
* @param {Object} ev
*/
async _onSendValue(ev) {
ev.stopPropagation();
await this.sendValue("", this.props.value, this.state.fileName);
}
/**
* Decrypt the file and download it
*/
async onFileDownload() {
if (!this.props.value) {
this.do_warn(
_lt("Save As..."),
_lt("The field is empty, there's nothing to save!")
);
} else if (utils.supported()) {
const decrypted = await this._decrypt(this.props.value);
const base64 = atob(decrypted);
const buffer = new ArrayBuffer(base64.length);
const arr = new Uint8Array(buffer);
for (let i = 0; i < base64.length; i++) arr[i] = base64.charCodeAt(i);
const blob = new Blob([arr]);
await downloadFile(blob, this.state.fileName || "");
}
}
}
VaultFile.displayName = _lt("Vault File");
VaultFile.template = "vault.FileVault";
registry.category("fields").add("vault_file", VaultFile);

View file

@ -0,0 +1,49 @@
/** @odoo-module alias=vault.inbox.field **/
// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import VaultField from "vault.field";
import VaultInboxMixin from "vault.inbox.mixin";
import {_lt} from "@web/core/l10n/translation";
import {registry} from "@web/core/registry";
import utils from "vault.utils";
import vault from "vault";
export default class VaultInboxField extends VaultInboxMixin(VaultField) {
/**
* Save the content in an entry of a vault
*
* @private
*/
async _onSaveValue() {
await this.saveValue("vault.field", this.props.value);
}
/**
* Decrypt the data with the private key of the vault
*
* @private
* @param {String} data
* @returns the decrypted data
*/
async _decrypt(data) {
if (!utils.supported()) return null;
const iv = this.props.record.data[this.props.fieldIV];
const wrapped_key = this.props.record.data[this.props.fieldKey];
if (!iv || !wrapped_key) return false;
const key = await vault.unwrap(wrapped_key);
return await utils.sym_decrypt(key, data, iv);
}
}
VaultInboxField.defaultProps = {
...VaultField.defaultProps,
fieldKey: "key",
};
VaultInboxField.displayName = _lt("Vault Inbox Field");
VaultInboxField.template = "vault.FieldVaultInbox";
registry.category("fields").add("vault_inbox_field", VaultInboxField);

View file

@ -0,0 +1,49 @@
/** @odoo-module alias=vault.inbox.file **/
// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import VaultFile from "vault.file";
import VaultInboxMixin from "vault.inbox.mixin";
import {_lt} from "@web/core/l10n/translation";
import {registry} from "@web/core/registry";
import utils from "vault.utils";
import vault from "vault";
export default class VaultInboxFile extends VaultInboxMixin(VaultFile) {
/**
* Save the content in an entry of a vault
*
* @private
*/
async _onSaveValue() {
await this.saveValue("vault.file", this.props.value, this.state.fileName);
}
/**
* Decrypt the data with the private key of the vault
*
* @private
* @param {String} data
* @returns the decrypted data
*/
async _decrypt(data) {
if (!utils.supported()) return null;
const iv = this.props.record.data[this.props.fieldIV];
const wrapped_key = this.props.record.data[this.props.fieldKey];
if (!iv || !wrapped_key) return false;
const key = await vault.unwrap(wrapped_key);
return await utils.sym_decrypt(key, data, iv);
}
}
VaultInboxFile.defaultProps = {
...VaultFile.defaultProps,
fieldKey: "key",
};
VaultInboxFile.displayName = _lt("Vault Inbox File");
VaultInboxFile.template = "vault.FileVaultInbox";
registry.category("fields").add("vault_inbox_file", VaultInboxFile);

View file

@ -0,0 +1,64 @@
/** @odoo-module alias=vault.inbox.mixin **/
// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import {_lt} from "@web/core/l10n/translation";
import {useService} from "@web/core/utils/hooks";
import utils from "vault.utils";
import vault from "vault";
export default (x) => {
class Extended extends x {
setup() {
super.setup();
if (!this.action) this.action = useService("action");
}
/**
* Save the content in an entry of a vault
*
* @private
* @param {String} model
* @param {String} value
* @param {String} name
*/
async saveValue(model, value, name = "") {
const key = await utils.generate_key();
const iv = utils.generate_iv_base64();
const decrypted = await this._decrypt(value);
this.action.doAction({
type: "ir.actions.act_window",
title: _lt("Store the secret in a vault"),
target: "new",
res_model: "vault.store.wizard",
views: [[false, "form"]],
context: {
default_model: model,
default_secret_temporary: await utils.sym_encrypt(
key,
decrypted,
iv
),
default_name: name,
default_iv: iv,
default_key: await vault.wrap(key),
},
});
}
}
Extended.props = {
...x.props,
storeModel: {type: String, optional: true},
};
Extended.extractProps = ({attrs}) => {
return {
storeModel: attrs.store,
};
};
return Extended;
};

View file

@ -0,0 +1,197 @@
/** @odoo-module alias=vault.mixin **/
// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import {_lt} from "@web/core/l10n/translation";
import {standardFieldProps} from "@web/views/fields/standard_field_props";
import utils from "vault.utils";
import vault from "vault";
export default (x) => {
class Extended extends x {
supported() {
return utils.supported();
}
// Control the visibility of the buttons
get showButton() {
return this.props.value;
}
get copyButton() {
return this.props.value;
}
get sendButton() {
return this.props.value;
}
get saveButton() {
return this.props.value;
}
get generateButton() {
return true;
}
get isNew() {
return Boolean(this.model.record.isNew);
}
/**
* Set the value by encrypting it
*
* @param {String} value
* @param {Object} options
*/
async storeValue(value, options) {
if (!utils.supported()) return;
const encrypted = await this._encrypt(value);
await this.props.update(encrypted, options);
}
/**
* Send the value to an inbox
*
* @param {String} value_field
* @param {String} value_file
* @param {String} filename
*/
async sendValue(value_field = "", value_file = "", filename = "") {
if (!utils.supported()) return;
if (!value_field && !value_file) return;
let enc_field = false,
enc_file = false;
// Prepare the key and iv for the reencryption
const key = await utils.generate_key();
const iv = utils.generate_iv_base64();
// Reencrypt the field
if (value_field) {
const decrypted = await this._decrypt(value_field);
enc_field = await utils.sym_encrypt(key, decrypted, iv);
}
// Reencrypt the file
if (value_file) {
const decrypted = await this._decrypt(value_file);
enc_file = await utils.sym_encrypt(key, decrypted, iv);
}
// Call the wizard to handle the user selection and storage
this.action.doAction({
type: "ir.actions.act_window",
title: _lt("Send the secret to another user"),
target: "new",
res_model: "vault.send.wizard",
views: [[false, "form"]],
context: {
default_secret: enc_field,
default_secret_file: enc_file,
default_filename: filename || false,
default_iv: iv,
default_key: await vault.wrap(key),
},
});
}
/**
* Set the value of a different field
*
* @param {String} field
* @param {String} value
*/
async _setFieldValue(field, value) {
this.props.record.update({[field]: value});
}
/**
* Extract the IV or generate a new one if needed
*
* @returns the IV to use
*/
async _getIV() {
if (!utils.supported()) return null;
// Read the IV from the field
let iv = this.props.record.data[this.props.fieldIV];
if (iv) return iv;
// Generate a new IV
iv = utils.generate_iv_base64();
await this._setFieldValue(this.props.fieldIV, iv);
return iv;
}
/**
* Extract the master key of the vault or generate a new one
*
* @returns the master key to use
*/
async _getMasterKey() {
if (!utils.supported()) return null;
// Check if the master key is already extracted
if (this.key) return await vault.unwrap(this.key);
// Get the wrapped master key from the field
this.key = this.props.record.data[this.props.fieldKey];
if (this.key) return await vault.unwrap(this.key);
// Generate a new master key and write it to the field
const key = await utils.generate_key();
this.key = await vault.wrap(key);
await this._setFieldValue(this.props.fieldKey, this.key);
return key;
}
/**
* Decrypt data with the master key stored in the vault
*
* @param {String} data
* @returns the decrypted data
*/
async _decrypt(data) {
if (!utils.supported()) return null;
const iv = await this._getIV();
const key = await this._getMasterKey();
return await utils.sym_decrypt(key, data, iv);
}
/**
* Encrypt data with the master key stored in the vault
*
* @param {String} data
* @returns the encrypted data
*/
async _encrypt(data) {
if (!utils.supported()) return null;
const iv = await this._getIV();
const key = await this._getMasterKey();
return await utils.sym_encrypt(key, data, iv);
}
}
Extended.defaultProps = {
...x.defaultProps,
fieldIV: "iv",
fieldKey: "master_key",
};
Extended.props = {
...standardFieldProps,
...x.props,
fieldKey: {type: String, optional: true},
fieldIV: {type: String, optional: true},
};
Extended.extractProps = ({attrs, field}) => {
const extract_props = x.extractProps || (() => ({}));
return {
...extract_props({attrs, field}),
fieldKey: attrs.key,
fieldIV: attrs.iv,
};
};
return Extended;
};