-
-
Notifications
You must be signed in to change notification settings - Fork 539
[18.0][ADD] vault_portal: portal access to shared vaults #983
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cvinh
wants to merge
1
commit into
OCA:18.0
Choose a base branch
from
invitu:18.0-add_vault_portal
base: 18.0
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| ============ | ||
| Vault Portal | ||
| ============ | ||
|
|
||
| .. | ||
| !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
| !! This file is generated by oca-gen-addon-readme !! | ||
| !! changes will be overwritten. !! | ||
| !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
| !! source digest: sha256:27a0f61908b59d809f7f5183639e02d495844f84d333e054a55b0deed6947c5e | ||
| !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
|
|
||
| .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png | ||
| :target: https://odoo-community.org/page/development-status | ||
| :alt: Beta | ||
| .. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png | ||
| :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html | ||
| :alt: License: AGPL-3 | ||
| .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github | ||
| :target: https://github.com/OCA/server-auth/tree/18.0/vault_portal | ||
| :alt: OCA/server-auth | ||
| .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png | ||
| :target: https://translation.odoo-community.org/projects/server-auth-18-0/server-auth-18-0-vault_portal | ||
| :alt: Translate me on Weblate | ||
| .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png | ||
| :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=18.0 | ||
| :alt: Try me on Runboat | ||
|
|
||
| |badge1| |badge2| |badge3| |badge4| |badge5| | ||
|
|
||
| Exposes vaults from the ``vault`` module to portal contacts | ||
| (``base.group_portal``), individually end-to-end encrypted like internal | ||
| users: each contact holds their own key pair, and the vault's master key | ||
| is re-wrapped for them. | ||
|
|
||
| Portal contacts can view the entries of vaults shared with them, and, | ||
| where granted per contact, edit existing field values, add new fields, | ||
| create new entries, and edit an entry's URL and expiry date. The vault | ||
| itself, entry names, and entry deletion remain read-only from the | ||
| portal. Entry tags are shown, read-only. | ||
|
|
||
| Entries can be searched by name, URL, and tag, and filtered by | ||
| active/expired status. A contact can also invalidate their own key pair | ||
| from the portal. | ||
|
|
||
| An optional MFA policy (Settings > Vault) can require two-factor | ||
| authentication for portal contacts before granting write access, or | ||
| before granting any access at all. | ||
|
|
||
| **Table of contents** | ||
|
|
||
| .. contents:: | ||
| :local: | ||
|
|
||
| Usage | ||
| ===== | ||
|
|
||
| 1. Grant portal access to the contact (partner form, "Grant portal | ||
| access"). | ||
| 2. The contact visits ``/my/vaults`` and clicks "Set up my key" there. | ||
| This works even with zero vaults shared yet: it generates their | ||
| personal key pair, a prerequisite for step 3. | ||
| 3. A technician adds a line in the desired vault's rights | ||
| (``right_ids``) for the contact's user, with the desired permissions. | ||
| Only contacts who already completed step 2 can be selected. | ||
|
|
||
| To revoke access, remove the ``vault.right`` line; the vault is flagged | ||
| for key rotation. | ||
|
|
||
| A contact can invalidate their own key pair from ``/my/vaults`` | ||
| ("Invalidate my key") if they lose their master password - this revokes | ||
| all their existing vault access, exactly like the backend's own | ||
| "Invalidate private key" action; a technician must then re-share access | ||
| with their new key. | ||
|
|
||
| The MFA policy is set in Settings > Vault > "Portal Two-Factor | ||
| Authentication Policy" (``none`` / ``write`` / ``read``). | ||
|
|
||
| Known issues / Roadmap | ||
| ====================== | ||
|
|
||
| - Read-only view of ``vault.log`` for portal contacts. | ||
| - Dedicated inactivity timeout for the portal vault session. | ||
| - Portal renaming of ``vault.entry``, deletion of entries and fields. | ||
| - Frontend test for the portal home tile visibility. | ||
| - No "Add an entry" control when a vault has zero entries yet. | ||
|
|
||
| Bug Tracker | ||
| =========== | ||
|
|
||
| Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-auth/issues>`_. | ||
| In case of trouble, please check there if your issue has already been reported. | ||
| If you spotted it first, help us to smash it by providing a detailed and welcomed | ||
| `feedback <https://github.com/OCA/server-auth/issues/new?body=module:%20vault_portal%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. | ||
|
|
||
| Do not contact contributors directly about support or help with technical issues. | ||
|
|
||
| Credits | ||
| ======= | ||
|
|
||
| Authors | ||
| ------- | ||
|
|
||
| * INVITU | ||
|
|
||
| Contributors | ||
| ------------ | ||
|
|
||
| - Cyril VINH-TUNG <<cyril@invitu.com>> | ||
|
|
||
| Maintainers | ||
| ----------- | ||
|
|
||
| This module is maintained by the OCA. | ||
|
|
||
| .. image:: https://odoo-community.org/logo.png | ||
| :alt: Odoo Community Association | ||
| :target: https://odoo-community.org | ||
|
|
||
| OCA, or the Odoo Community Association, is a nonprofit organization whose | ||
| mission is to support the collaborative development of Odoo features and | ||
| promote its widespread use. | ||
|
|
||
| This module is part of the `OCA/server-auth <https://github.com/OCA/server-auth/tree/18.0/vault_portal>`_ project on GitHub. | ||
|
|
||
| You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Copyright 2026 INVITU (<https://www.invitu.com>) | ||
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
|
||
| from . import controllers | ||
| from . import models |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # Copyright 2026 INVITU (<https://www.invitu.com>) | ||
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
|
||
| { | ||
| "name": "Vault Portal", | ||
| "summary": "Portal access to shared vaults, per-user E2E encrypted", | ||
| "license": "AGPL-3", | ||
| "version": "18.0.1.0.0", | ||
| "website": "https://github.com/OCA/server-auth", | ||
| "author": "INVITU, Odoo Community Association (OCA)", | ||
| "category": "Vault", | ||
| "depends": ["vault", "portal", "auth_totp"], | ||
| "data": [ | ||
| "security/ir.model.access.csv", | ||
| "security/ir_rule.xml", | ||
| "views/portal_templates.xml", | ||
| "views/res_config_settings_views.xml", | ||
| ], | ||
| "assets": { | ||
| "web.assets_frontend": [ | ||
| "vault/static/src/backend/vault.esm.js", | ||
| "vault_portal/static/src/frontend/vault_key_manager.esm.js", | ||
| "vault_portal/static/src/frontend/vault_detail.esm.js", | ||
| "vault_portal/static/src/xml/vault_key_manager.xml", | ||
| "vault_portal/static/src/xml/vault_detail.xml", | ||
| "vault_portal/static/src/scss/portal.scss", | ||
| ], | ||
| }, | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Copyright 2026 INVITU (<https://www.invitu.com>) | ||
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
|
||
| from . import portal |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| # Copyright 2026 INVITU (<https://www.invitu.com>) | ||
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
|
||
| import json | ||
|
|
||
| from odoo import _ | ||
| from odoo.exceptions import AccessError, UserError | ||
| from odoo.http import request, route | ||
|
|
||
| from odoo.addons.portal.controllers.portal import CustomerPortal | ||
|
|
||
|
|
||
| class VaultPortal(CustomerPortal): | ||
| def _prepare_home_portal_values(self, counters): | ||
| values = super()._prepare_home_portal_values(counters) | ||
| if "vault_count" in counters: | ||
| values["vault_count"] = request.env["vault"].search_count([]) | ||
| return values | ||
|
|
||
| def _vault_portal_base_values(self): | ||
| values = self._prepare_portal_layout_values() | ||
| values.update( | ||
| { | ||
| "has_keys": bool(request.env.user.active_key), | ||
| "page_name": "vaults", | ||
| } | ||
| ) | ||
| return values | ||
|
|
||
| def _vault_portal_searchbar_filters(self): | ||
| return { | ||
| "all": {"label": _("All"), "domain": []}, | ||
| "active": {"label": _("Active"), "domain": [("expired", "=", False)]}, | ||
| "expired": {"label": _("Expired"), "domain": [("expired", "=", True)]}, | ||
| } | ||
|
|
||
| def _get_vault_or_404(self, vault_id): | ||
| vault = request.env["vault"].browse(vault_id).exists() | ||
| if not vault: | ||
| raise request.not_found() | ||
| try: | ||
| vault.check_access("read") | ||
| except AccessError: | ||
| raise request.not_found() from None | ||
| return vault | ||
|
|
||
| def _get_vault_record(self, vault, model, record_id, label): | ||
| record = request.env[model].browse(record_id).exists() | ||
| if not record or record.vault_id != vault: | ||
| raise UserError(_("%s not found in this vault.") % label) | ||
| return record | ||
|
|
||
| def _field_props(self, field): | ||
| return { | ||
| "id": field.id, | ||
| "name": field.name, | ||
| "value": field.value, | ||
| "iv": field.iv, | ||
| "allowedWrite": field.allowed_write, | ||
| } | ||
|
|
||
| def _entry_props(self, entry): | ||
| return { | ||
| "id": entry.id, | ||
| "name": entry.name, | ||
| "url": entry.url or "", | ||
| "expireDate": entry.expire_date | ||
| and entry.expire_date.date().isoformat() | ||
| or "", | ||
| "tags": entry.tags.mapped("name"), | ||
| "expired": entry.expired, | ||
| "allowedWrite": entry.allowed_write, | ||
| "allowedCreate": entry.allowed_create, | ||
| "parentId": entry.parent_id.id or 0, | ||
| "parentName": entry.parent_id.complete_name if entry.parent_id else "", | ||
| "fields": [self._field_props(f) for f in entry.field_ids], | ||
| } | ||
|
|
||
| def _vault_props(self, vault, entries): | ||
| return { | ||
| "vaultId": vault.id, | ||
| "masterKey": vault.master_key or "", | ||
| "allowedCreate": vault.allowed_create, | ||
| "entries": [self._entry_props(e) for e in entries], | ||
| } | ||
|
|
||
| @route(["/my/vaults"], type="http", auth="user", website=True) | ||
| def portal_my_vaults(self, **kwargs): | ||
| values = self._vault_portal_base_values() | ||
| values["vaults"] = request.env["vault"].search([]) | ||
| values["key_manager_props"] = json.dumps({"hasKeys": values["has_keys"]}) | ||
| return request.render("vault_portal.portal_my_vaults", values) | ||
|
|
||
| @route(["/my/vaults/<int:vault_id>"], type="http", auth="user", website=True) | ||
| def portal_vault_detail(self, vault_id, filterby=None, **kwargs): | ||
| vault = self._get_vault_or_404(vault_id) | ||
|
|
||
| searchbar_filters = self._vault_portal_searchbar_filters() | ||
| if not filterby or filterby not in searchbar_filters: | ||
| filterby = "all" | ||
| domain = [("vault_id", "=", vault.id)] + searchbar_filters[filterby]["domain"] | ||
|
|
||
| entries = ( | ||
| request.env["vault.entry"] | ||
| .search(domain) | ||
| # Hide pure organizational folders (no field of their own, | ||
| # but with children shown as their own group) - never hide | ||
| # a genuinely empty leaf entry (no field, no child yet), | ||
| # which would otherwise make a freshly created entry vanish | ||
| # with no way to add a first field to it. | ||
| .filtered(lambda e: e.field_ids or not e.child_ids) | ||
| .sorted(key=lambda e: (e.parent_id.id, e.complete_name)) | ||
| ) | ||
|
|
||
| values = self._vault_portal_base_values() | ||
| values.update( | ||
| { | ||
| "vault": vault, | ||
| "searchbar_filters": searchbar_filters, | ||
| "filterby": filterby, | ||
| "default_url": f"/my/vaults/{vault.id}", | ||
| "vault_props": json.dumps(self._vault_props(vault, entries)), | ||
| } | ||
| ) | ||
| return request.render("vault_portal.portal_vault_detail", values) | ||
|
|
||
| @route(["/my/vaults/<int:vault_id>/entries"], type="json", auth="user") | ||
| def portal_vault_create_entry(self, vault_id, name, parent_id=0, **kwargs): | ||
| vault = self._get_vault_or_404(vault_id) | ||
| vals = {"vault_id": vault.id, "name": name} | ||
| if parent_id: | ||
| vals["parent_id"] = parent_id | ||
| entry = request.env["vault.entry"].create(vals) | ||
| return self._entry_props(entry) | ||
|
|
||
| @route( | ||
| ["/my/vaults/<int:vault_id>/entries/<int:entry_id>"], | ||
| type="json", | ||
| auth="user", | ||
| ) | ||
| def portal_vault_write_entry(self, vault_id, entry_id, **vals): | ||
| vault = self._get_vault_or_404(vault_id) | ||
| entry = self._get_vault_record(vault, "vault.entry", entry_id, _("Entry")) | ||
| entry.write(vals) | ||
| return self._entry_props(entry) | ||
|
|
||
| @route( | ||
| ["/my/vaults/<int:vault_id>/entries/<int:entry_id>/fields"], | ||
| type="json", | ||
| auth="user", | ||
| ) | ||
| def portal_vault_create_field(self, vault_id, entry_id, name, value, iv, **kwargs): | ||
| vault = self._get_vault_or_404(vault_id) | ||
| entry = self._get_vault_record(vault, "vault.entry", entry_id, _("Entry")) | ||
| field = request.env["vault.field"].create( | ||
| {"entry_id": entry.id, "name": name, "value": value, "iv": iv} | ||
| ) | ||
| return self._field_props(field) | ||
|
|
||
| @route( | ||
| ["/my/vaults/<int:vault_id>/fields/<int:field_id>"], | ||
| type="json", | ||
| auth="user", | ||
| ) | ||
| def portal_vault_write_field(self, vault_id, field_id, value, iv, **kwargs): | ||
| vault = self._get_vault_or_404(vault_id) | ||
| field = self._get_vault_record(vault, "vault.field", field_id, _("Field")) | ||
| field.write({"value": value, "iv": iv}) | ||
| return self._field_props(field) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you also want to support files? I don't see it anywhere and not even listed in the roadmap. |
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Copyright 2026 INVITU (<https://www.invitu.com>) | ||
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
|
||
| from . import res_config_settings | ||
| from . import res_users | ||
| from . import vault_entry | ||
| from . import vault_right |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Copyright 2026 INVITU (<https://www.invitu.com>) | ||
| # 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" | ||
|
|
||
| vault_portal_mfa_policy = fields.Selection( | ||
| [ | ||
| ("none", "No constraint"), | ||
| ("write", "Require 2FA for write/create access"), | ||
| ("read", "Require 2FA for any portal access"), | ||
| ], | ||
| string="Portal contacts MFA policy", | ||
| config_parameter="vault_portal.mfa_policy", | ||
| default="none", | ||
| help=( | ||
| "No constraint: two-factor authentication is not required.\n" | ||
| "Require 2FA for write/create access: a portal contact must " | ||
| "have 2FA enabled before being granted write or create access " | ||
| "to a vault; disabling 2FA afterwards downgrades their access " | ||
| "back to read-only.\n" | ||
| "Require 2FA for any portal access: a portal contact must have " | ||
| "2FA enabled before being granted ANY access to a vault, " | ||
| "including read-only; disabling 2FA afterwards immediately " | ||
| "revokes all their vault access." | ||
| ), | ||
| ) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
vault_utilsare missing.