From 93f99f4978d6bf860695b12f17f38e2058c3ed34 Mon Sep 17 00:00:00 2001 From: Adrien Peiffer Date: Fri, 3 Mar 2023 16:00:15 +0100 Subject: [PATCH 001/112] Add auth_oauth_ropc --- auth_oauth_ropc/README.rst | 104 ++++ auth_oauth_ropc/__init__.py | 1 + auth_oauth_ropc/__manifest__.py | 17 + auth_oauth_ropc/models/__init__.py | 2 + auth_oauth_ropc/models/oauth_ropc_provider.py | 44 ++ auth_oauth_ropc/models/res_users.py | 23 + auth_oauth_ropc/readme/CONFIGURE.rst | 11 + auth_oauth_ropc/readme/CONTRIBUTORS.rst | 1 + auth_oauth_ropc/readme/DESCRIPTION.rst | 7 + auth_oauth_ropc/readme/USAGE.rst | 5 + .../security/oauth_ropc_provider.xml | 16 + .../static/description/configuration.png | Bin 0 -> 25734 bytes auth_oauth_ropc/static/description/icon.png | Bin 0 -> 9455 bytes auth_oauth_ropc/static/description/index.html | 443 ++++++++++++++++++ auth_oauth_ropc/views/oauth_ropc_provider.xml | 53 +++ .../odoo/addons/auth_oauth_ropc | 1 + setup/auth_oauth_ropc/setup.py | 6 + 17 files changed, 734 insertions(+) create mode 100644 auth_oauth_ropc/README.rst create mode 100644 auth_oauth_ropc/__init__.py create mode 100644 auth_oauth_ropc/__manifest__.py create mode 100644 auth_oauth_ropc/models/__init__.py create mode 100644 auth_oauth_ropc/models/oauth_ropc_provider.py create mode 100644 auth_oauth_ropc/models/res_users.py create mode 100644 auth_oauth_ropc/readme/CONFIGURE.rst create mode 100644 auth_oauth_ropc/readme/CONTRIBUTORS.rst create mode 100644 auth_oauth_ropc/readme/DESCRIPTION.rst create mode 100644 auth_oauth_ropc/readme/USAGE.rst create mode 100644 auth_oauth_ropc/security/oauth_ropc_provider.xml create mode 100644 auth_oauth_ropc/static/description/configuration.png create mode 100644 auth_oauth_ropc/static/description/icon.png create mode 100644 auth_oauth_ropc/static/description/index.html create mode 100644 auth_oauth_ropc/views/oauth_ropc_provider.xml create mode 120000 setup/auth_oauth_ropc/odoo/addons/auth_oauth_ropc create mode 100644 setup/auth_oauth_ropc/setup.py diff --git a/auth_oauth_ropc/README.rst b/auth_oauth_ropc/README.rst new file mode 100644 index 0000000000..63dab04b84 --- /dev/null +++ b/auth_oauth_ropc/README.rst @@ -0,0 +1,104 @@ +=============== +Auth OAuth ROPC +=============== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/16.0/auth_oauth_ropc + :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-16-0/server-auth-16-0-auth_oauth_ropc + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/251/16.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module add the possibility to login with OAuth Resource Owner Password Credentials Grant + +https://datatracker.ietf.org/doc/html/rfc6749#section-4.3 + +In most scenarios, more secure alternatives are available and recommended. This flow requires a very high degree of trust in the application, and carries risks that are not present in other flows. You should only use this flow when other more secure flows aren't viable. + +This module is useful for the Odoo mobile application, which only supports user/password authentication. + + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +The configuration of this module is based with Microsoft Azure ad OAuth provider + +https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc + +To configure this module, you need to: + +#. Go to Settings/Users/OAuth ROPC providers and create a new one + +.. figure:: https://raw.githubusercontent.com/OCA/server-auth/16.0/auth_oauth_ropc/static/description/configuration.png + :alt: provider description + :width: 600 px + +Usage +===== + +To use this module, you need to: + +#. Go on the login screen +#. Fill your Odoo user name (must be the same in OAuth provider) +#. Fill your OAuth password + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +Adrien Peiffer + +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 `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/auth_oauth_ropc/__init__.py b/auth_oauth_ropc/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/auth_oauth_ropc/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/auth_oauth_ropc/__manifest__.py b/auth_oauth_ropc/__manifest__.py new file mode 100644 index 0000000000..f055dc539f --- /dev/null +++ b/auth_oauth_ropc/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Auth OAuth ROPC", + "summary": """ + Allow to login with OAuth Resource Owner Password Credentials Grant""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-auth", + "depends": ["base"], + "data": [ + "security/oauth_ropc_provider.xml", + "views/oauth_ropc_provider.xml", + ], +} diff --git a/auth_oauth_ropc/models/__init__.py b/auth_oauth_ropc/models/__init__.py new file mode 100644 index 0000000000..c136e1765e --- /dev/null +++ b/auth_oauth_ropc/models/__init__.py @@ -0,0 +1,2 @@ +from . import oauth_ropc_provider +from . import res_users diff --git a/auth_oauth_ropc/models/oauth_ropc_provider.py b/auth_oauth_ropc/models/oauth_ropc_provider.py new file mode 100644 index 0000000000..095c4abf3e --- /dev/null +++ b/auth_oauth_ropc/models/oauth_ropc_provider.py @@ -0,0 +1,44 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import requests + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class OAuthRopcProvider(models.Model): + + _name = "oauth.ropc.provider" + _description = "OAuth ROPC Provider" + + name = fields.Char() + client_id = fields.Char(string="Client ID") + client_secret = fields.Char() + auth_endpoint = fields.Char(string="Authorization URL", required=True) + resource = fields.Char() + scope = fields.Char() + active = fields.Boolean(default=True) + + @api.constrains("active") + def _check_active(self): + records_to_check = self.filtered(lambda r: r.active) + for record in records_to_check: + if self.search([("id", "!=", record.id)]): + raise ValidationError(_("""You can define only one active provider""")) + + def _authenticate(self, login, password): + self.ensure_one() + data = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "resource": self.resource, + "scope": self.scope, + "grant_type": "password", + "username": login, + "password": password, + } + r = requests.post(self.auth_endpoint, data=data, timeout=5) + if r.status_code == 200: + return True + return False diff --git a/auth_oauth_ropc/models/res_users.py b/auth_oauth_ropc/models/res_users.py new file mode 100644 index 0000000000..3bf8dff43d --- /dev/null +++ b/auth_oauth_ropc/models/res_users.py @@ -0,0 +1,23 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models +from odoo.exceptions import AccessDenied + + +class ResUsers(models.Model): + + _inherit = "res.users" + + def _check_credentials(self, password, env): + try: + return super(ResUsers, self)._check_credentials(password, env) + except AccessDenied: + passwd_allowed = ( + env["interactive"] or not self.env.user._rpc_api_keys_only() + ) + if passwd_allowed and self.env.user.active: + if ropc_provider := self.env["oauth.ropc.provider"].sudo().search([]): + if ropc_provider._authenticate(self.env.user.login, password): + return + raise diff --git a/auth_oauth_ropc/readme/CONFIGURE.rst b/auth_oauth_ropc/readme/CONFIGURE.rst new file mode 100644 index 0000000000..7ade86e028 --- /dev/null +++ b/auth_oauth_ropc/readme/CONFIGURE.rst @@ -0,0 +1,11 @@ +The configuration of this module is based with Microsoft Azure ad OAuth provider + +https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc + +To configure this module, you need to: + +#. Go to Settings/Users/OAuth ROPC providers and create a new one + +.. figure:: ../static/description/configuration.png + :alt: provider description + :width: 600 px diff --git a/auth_oauth_ropc/readme/CONTRIBUTORS.rst b/auth_oauth_ropc/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..e2bc6777dc --- /dev/null +++ b/auth_oauth_ropc/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +Adrien Peiffer diff --git a/auth_oauth_ropc/readme/DESCRIPTION.rst b/auth_oauth_ropc/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..d976f67902 --- /dev/null +++ b/auth_oauth_ropc/readme/DESCRIPTION.rst @@ -0,0 +1,7 @@ +This module add the possibility to login with OAuth Resource Owner Password Credentials Grant + +https://datatracker.ietf.org/doc/html/rfc6749#section-4.3 + +In most scenarios, more secure alternatives are available and recommended. This flow requires a very high degree of trust in the application, and carries risks that are not present in other flows. You should only use this flow when other more secure flows aren't viable. + +This module is usefull for the Odoo mobile application, which only supports user/password authentication. diff --git a/auth_oauth_ropc/readme/USAGE.rst b/auth_oauth_ropc/readme/USAGE.rst new file mode 100644 index 0000000000..2b8eb9cdaa --- /dev/null +++ b/auth_oauth_ropc/readme/USAGE.rst @@ -0,0 +1,5 @@ +To use this module, you need to: + +#. Go on the login screen +#. Fill your Odoo user name (must be the same in OAuth provider) +#. Fill your OAuth password diff --git a/auth_oauth_ropc/security/oauth_ropc_provider.xml b/auth_oauth_ropc/security/oauth_ropc_provider.xml new file mode 100644 index 0000000000..dfb9201231 --- /dev/null +++ b/auth_oauth_ropc/security/oauth_ropc_provider.xml @@ -0,0 +1,16 @@ + + + + + + oauth.ropc.provider access system + + + + + + + + + diff --git a/auth_oauth_ropc/static/description/configuration.png b/auth_oauth_ropc/static/description/configuration.png new file mode 100644 index 0000000000000000000000000000000000000000..370233cacddc90500ef2ab942a0bc282cef2245c GIT binary patch literal 25734 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�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 literal 0 HcmV?d00001 diff --git a/auth_oauth_ropc/static/description/icon.png b/auth_oauth_ropc/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/auth_oauth_ropc/static/description/index.html b/auth_oauth_ropc/static/description/index.html new file mode 100644 index 0000000000..a100600ffa --- /dev/null +++ b/auth_oauth_ropc/static/description/index.html @@ -0,0 +1,443 @@ + + + + + + +Auth OAuth ROPC + + + +
+

Auth OAuth ROPC

+ + +

Beta License: AGPL-3 OCA/server-auth Translate me on Weblate Try me on Runbot

+

This module add the possibility to login with OAuth Resource Owner Password Credentials Grant

+

https://datatracker.ietf.org/doc/html/rfc6749#section-4.3

+

In most scenarios, more secure alternatives are available and recommended. This flow requires a very high degree of trust in the application, and carries risks that are not present in other flows. You should only use this flow when other more secure flows aren’t viable.

+

This module is usefull for the Odoo mobile application, which only supports user/password authentication.

+

Table of contents

+ +
+

Configuration

+

The configuration of this module is based with Microsoft Azure ad OAuth provider

+

https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc

+

To configure this module, you need to:

+
    +
  1. Go to Settings/Users/OAuth ROPC providers and create a new one
  2. +
+
+provider description +
+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Go on the login screen
  2. +
  3. Fill your Odoo user name (must be the same in OAuth provider)
  4. +
  5. Fill your OAuth password
  6. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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 project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/auth_oauth_ropc/views/oauth_ropc_provider.xml b/auth_oauth_ropc/views/oauth_ropc_provider.xml new file mode 100644 index 0000000000..ee69abb61b --- /dev/null +++ b/auth_oauth_ropc/views/oauth_ropc_provider.xml @@ -0,0 +1,53 @@ + + + + + + oauth.ropc.provider.form (in auth_oauth_ropc) + oauth.ropc.provider + +
+ + + + + + + + + + + +
+
+
+ + + + oauth.ropc.provider.tree (in auth_oauth_ropc) + oauth.ropc.provider + + + + + + + + + + oauth ROPC Providers + oauth.ropc.provider + tree,form + [] + {} + + + + oauth ROPC Providers + + + + + +
diff --git a/setup/auth_oauth_ropc/odoo/addons/auth_oauth_ropc b/setup/auth_oauth_ropc/odoo/addons/auth_oauth_ropc new file mode 120000 index 0000000000..d5d7c3d385 --- /dev/null +++ b/setup/auth_oauth_ropc/odoo/addons/auth_oauth_ropc @@ -0,0 +1 @@ +../../../../auth_oauth_ropc \ No newline at end of file diff --git a/setup/auth_oauth_ropc/setup.py b/setup/auth_oauth_ropc/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/auth_oauth_ropc/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 35cf0217b02706f3ed35251ef22ee6c52df87950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 1 Jul 2023 17:38:31 +0200 Subject: [PATCH 002/112] Update auth_oauth_ropc/readme/DESCRIPTION.rst --- auth_oauth_ropc/readme/DESCRIPTION.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_oauth_ropc/readme/DESCRIPTION.rst b/auth_oauth_ropc/readme/DESCRIPTION.rst index d976f67902..79a7b4ffb5 100644 --- a/auth_oauth_ropc/readme/DESCRIPTION.rst +++ b/auth_oauth_ropc/readme/DESCRIPTION.rst @@ -4,4 +4,4 @@ https://datatracker.ietf.org/doc/html/rfc6749#section-4.3 In most scenarios, more secure alternatives are available and recommended. This flow requires a very high degree of trust in the application, and carries risks that are not present in other flows. You should only use this flow when other more secure flows aren't viable. -This module is usefull for the Odoo mobile application, which only supports user/password authentication. +This module is useful for the Odoo mobile application, which only supports user/password authentication. From eed91236f065f14d069b31a89ad97336ed75b4e3 Mon Sep 17 00:00:00 2001 From: Riccardo Bellanova Date: Fri, 15 Dec 2023 10:43:43 +0000 Subject: [PATCH 003/112] Translated using Weblate (Italian) Currently translated at 89.0% (57 of 64 strings) Translation: server-auth-16.0/server-auth-16.0-auth_jwt Translate-URL: https://translation.odoo-community.org/projects/server-auth-16-0/server-auth-16-0-auth_jwt/it/ --- auth_jwt/i18n/it.po | 140 ++++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 63 deletions(-) diff --git a/auth_jwt/i18n/it.po b/auth_jwt/i18n/it.po index 3b337b274b..9336d05128 100644 --- a/auth_jwt/i18n/it.po +++ b/auth_jwt/i18n/it.po @@ -6,13 +6,15 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2023-12-15 13:34+0000\n" +"Last-Translator: Riccardo Bellanova \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" #. module: auth_jwt #. odoo-python @@ -22,21 +24,23 @@ msgid "" "A cookie name must be provided on JWT validator %s because it has cookie " "mode enabled." msgstr "" +"È necessario fornire un nome del cookie sul validatore JWT %s perché ha la " +"modalità cookie abilitata." #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "Algorithm" -msgstr "" +msgstr "Algoritmo" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__audience msgid "Audience" -msgstr "" +msgstr "Audience" #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__audience msgid "Comma separated list of audiences, to validate aud." -msgstr "" +msgstr "Elenco di audience separati da virgole, per validare aud." #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_enabled @@ -45,303 +49,313 @@ msgid "" "Authorization header and the cookie are present in the request, the cookie " "is ignored." msgstr "" +"Converti il token JWT in un cookie HttpOnly Secure. Quando nella richiesta " +"sono presenti sia un Authorization header che il cookie, il cookie viene " +"ignorato." #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "Cookie" -msgstr "" +msgstr "Cookie" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_enabled msgid "Cookie Enabled" -msgstr "" +msgstr "Cookie abilitato" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_max_age msgid "Cookie Max Age" -msgstr "" +msgstr "Durata massima cookie" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_name msgid "Cookie Name" -msgstr "" +msgstr "Nome cookie" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_path msgid "Cookie Path" -msgstr "" +msgstr "Path cookie" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_secure +#, fuzzy msgid "Cookie Secure" -msgstr "" +msgstr "Cookie Secure" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_uid msgid "Created by" -msgstr "" +msgstr "Creato da" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_date msgid "Created on" -msgstr "" +msgstr "Creato il" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__display_name msgid "Display Name" -msgstr "" +msgstr "Nome visualizzato" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es256 msgid "ES256 - ECDSA using SHA-256" -msgstr "" +msgstr "ES256 - ECDSA usando SHA-256" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es256k msgid "ES256K - ECDSA with secp256k1 curve using SHA-256" -msgstr "" +msgstr "ES256K - ECDSA con curva secp256k1 usando SHA-256" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es384 msgid "ES384 - ECDSA using SHA-384" -msgstr "" +msgstr "ES384 - ECDSA usando SHA-384" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es512 msgid "ES512 - ECDSA using SHA-512" -msgstr "" +msgstr "ES512 - ECDSA usando SHA-512" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__partner_id_strategy__email +#, fuzzy msgid "From email claim" -msgstr "" +msgstr "Dalla richiesta email" #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "General" -msgstr "" +msgstr "Generale" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs256 msgid "HS256 - HMAC using SHA-256 hash algorithm" -msgstr "" +msgstr "HS256 - HMAC usando SHA-256 hash algorithm" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs384 msgid "HS384 - HMAC using SHA-384 hash algorithm" -msgstr "" +msgstr "HS384 - HMAC usando SHA-384 hash algorithm" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs512 msgid "HS512 - HMAC using SHA-512 hash algorithm" -msgstr "" +msgstr "HS512 - HMAC usando SHA-512 hash algorithm" #. module: auth_jwt #: model:ir.model,name:auth_jwt.model_ir_http msgid "HTTP Routing" -msgstr "" +msgstr "Instradamento HTTP" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__id +#, fuzzy msgid "ID" -msgstr "" +msgstr "ID" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__issuer +#, fuzzy msgid "Issuer" -msgstr "" +msgstr "Issuer" #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +#, fuzzy msgid "JWK URI" -msgstr "" +msgstr "JWK URI" #. module: auth_jwt #: model:ir.model,name:auth_jwt.model_auth_jwt_validator msgid "JWT Validator Configuration" -msgstr "" +msgstr "Configurazione validatore JWT" #. module: auth_jwt #: model:ir.actions.act_window,name:auth_jwt.action_auth_jwt_validator #: model:ir.ui.menu,name:auth_jwt.menu_auth_jwt_validator msgid "JWT Validators" -msgstr "" +msgstr "Validatori JWT" #. module: auth_jwt #: model:ir.model.constraint,message:auth_jwt.constraint_auth_jwt_validator_name_uniq msgid "JWT validator names must be unique !" -msgstr "" +msgstr "I nomi dei validatori JWT devono essere univoci!" #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "Key" -msgstr "" +msgstr "Chiave" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator____last_update msgid "Last Modified on" -msgstr "" +msgstr "Ultima modifica il" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Ultimo aggiornamento da" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__write_date msgid "Last Updated on" -msgstr "" +msgstr "Ultimo aggiornamento il" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__name msgid "Name" -msgstr "" +msgstr "Nome" #. module: auth_jwt #. odoo-python #: code:addons/auth_jwt/models/auth_jwt_validator.py:0 #, python-format msgid "Name %r is not a valid python identifier." -msgstr "" +msgstr "Il nome %r non è un identificatore Python valido." #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__next_validator_id msgid "Next Validator" -msgstr "" +msgstr "Validatore successivo" #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__next_validator_id msgid "Next validator to try if this one fails" -msgstr "" +msgstr "Validatore successivo da provare se questo fallisce" #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_max_age msgid "Number of seconds until the cookie expires (Max-Age)." -msgstr "" +msgstr "Numero di secondi fino alla scadenza del cookie (Durata max)." #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps256 msgid "PS256 - RSASSA-PSS using SHA-256 and MGF1 padding with SHA-256" -msgstr "" +msgstr "PS256 - RSASSA-PSS usando SHA-256 e padding MGF1 con SHA-256" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps384 msgid "PS384 - RSASSA-PSS using SHA-384 and MGF1 padding with SHA-384" -msgstr "" +msgstr "PS384 - RSASSA-PSS usando SHA-384 e padding MGF1 con SHA-384" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps512 msgid "PS512 - RSASSA-PSS using SHA-512 and MGF1 padding with SHA-512" -msgstr "" +msgstr "PS512 - RSASSA-PSS usando SHA-512 e padding MGF1 con SHA-512" #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +#, fuzzy msgid "Partner" -msgstr "" +msgstr "Partner" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_required msgid "Partner Id Required" -msgstr "" +msgstr "Partner ID obbligatorio" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_strategy msgid "Partner Id Strategy" -msgstr "" +msgstr "Strategia Partner ID" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__public_key_algorithm msgid "Public Key Algorithm" -msgstr "" +msgstr "Algoritmo a chiave pubblica" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__public_key_jwk_uri msgid "Public Key Jwk Uri" -msgstr "" +msgstr "Jwk Uri a chiave pubblica" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__signature_type__public_key msgid "Public key" -msgstr "" +msgstr "Chiave pubblica" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs256 msgid "RS256 - RSASSA-PKCS1-v1_5 using SHA-256" -msgstr "" +msgstr "RS256 - RSASSA-PKCS1-v1_5 usando SHA-256" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs384 msgid "RS384 - RSASSA-PKCS1-v1_5 using SHA-384" -msgstr "" +msgstr "RS384 - RSASSA-PKCS1-v1_5 usando SHA-384" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs512 msgid "RS512 - RSASSA-PKCS1-v1_5 using SHA-512" -msgstr "" +msgstr "RS512 - RSASSA-PKCS1-v1_5 usando SHA-512" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__signature_type__secret msgid "Secret" -msgstr "" +msgstr "Segreta" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_algorithm msgid "Secret Algorithm" -msgstr "" +msgstr "Algoritmo segreto" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_key msgid "Secret Key" -msgstr "" +msgstr "Chiave segreta" #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_secure msgid "Set to false only for development without https." -msgstr "" +msgstr "Imposta su false solo per lo sviluppo senza https." #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__signature_type msgid "Signature Type" -msgstr "" +msgstr "Tipo di firma" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__user_id_strategy__static msgid "Static" -msgstr "" +msgstr "Statica" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__static_user_id msgid "Static User" -msgstr "" +msgstr "Utente statico" #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__issuer msgid "To validate iss." -msgstr "" +msgstr "Per validare iss." #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "Token validation" -msgstr "" +msgstr "Convalida del token" #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "User" -msgstr "" +msgstr "Utente" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__user_id_strategy msgid "User Id Strategy" -msgstr "" +msgstr "Strategia User ID" #. module: auth_jwt #. odoo-python #: code:addons/auth_jwt/models/auth_jwt_validator.py:0 #, python-format msgid "Validators mustn't make a closed chain: {}." -msgstr "" +msgstr "I validatori non devono creare una catena chiusa: {}." #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +#, fuzzy msgid "arch" -msgstr "" +msgstr "arch" From 4e1f42eeb66fc5547b11949f5cbb7f2bdb8964ed Mon Sep 17 00:00:00 2001 From: Torvald Baade Bringsvor Date: Fri, 15 Dec 2023 14:44:29 +0100 Subject: [PATCH 004/112] Small bugfix, RelayState can be empty --- auth_saml/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index 8f5bed2272..bfd201aad2 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -191,7 +191,7 @@ def signin(self, **kw): """ saml_response = kw.get("SAMLResponse") - if kw.get("RelayState") is None: + if not kw.get("RelayState"): # here we are in front of a client that went through # some routes that "lost" its relaystate... this can happen # if the client visited his IDP and successfully logged in From 2a0e4955879ea8efecf23deb0f540e490bdd406b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 15 Dec 2023 13:53:48 +0000 Subject: [PATCH 005/112] [BOT] post-merge updates --- README.md | 2 +- auth_saml/README.rst | 2 +- auth_saml/__manifest__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ae01ce6743..e0cb4ba365 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ addon | version | maintainers | summary [auth_ldaps](auth_ldaps/) | 16.0.1.0.0 | | Allows to use LDAP over SSL authentication [auth_oidc](auth_oidc/) | 16.0.1.0.2 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Allow users to login through OpenID Connect Provider [auth_oidc_environment](auth_oidc_environment/) | 16.0.1.0.0 | | This module allows to use server env for OIDC configuration -[auth_saml](auth_saml/) | 16.0.1.0.2 | [![vincent-hatakeyama](https://github.com/vincent-hatakeyama.png?size=30px)](https://github.com/vincent-hatakeyama) | SAML2 Authentication +[auth_saml](auth_saml/) | 16.0.1.0.3 | [![vincent-hatakeyama](https://github.com/vincent-hatakeyama.png?size=30px)](https://github.com/vincent-hatakeyama) | SAML2 Authentication [auth_session_timeout](auth_session_timeout/) | 16.0.1.0.0 | | This module disable all inactive sessions since a given delay [auth_signup_verify_email](auth_signup_verify_email/) | 16.0.1.0.0 | | Force uninvited users to use a good email for signup [auth_user_case_insensitive](auth_user_case_insensitive/) | 16.0.1.0.0 | | Makes the user login field case insensitive diff --git a/auth_saml/README.rst b/auth_saml/README.rst index 0278f10326..ade85034a7 100644 --- a/auth_saml/README.rst +++ b/auth_saml/README.rst @@ -7,7 +7,7 @@ SAML2 Authentication !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:d297addccec609ee677847489b8f99d0da3056d322226eef9cb790c6bbe9667a + !! source digest: sha256:1e046a7179ace3d0932313947c9156983197334815735ec52428916f26e3d354 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/auth_saml/__manifest__.py b/auth_saml/__manifest__.py index 4cb9bb7de1..102f9dd209 100644 --- a/auth_saml/__manifest__.py +++ b/auth_saml/__manifest__.py @@ -4,7 +4,7 @@ { "name": "SAML2 Authentication", - "version": "16.0.1.0.2", + "version": "16.0.1.0.3", "category": "Tools", "author": "XCG Consulting, Odoo Community Association (OCA)", "maintainers": ["vincent-hatakeyama"], From 75f1fac8cbf25d266df9df694811f58066cba2cb Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 19 Dec 2023 09:51:27 +0000 Subject: [PATCH 006/112] [UPD] Update auth_oauth_ropc.pot --- auth_oauth_ropc/i18n/auth_oauth_ropc.pot | 107 +++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 auth_oauth_ropc/i18n/auth_oauth_ropc.pot diff --git a/auth_oauth_ropc/i18n/auth_oauth_ropc.pot b/auth_oauth_ropc/i18n/auth_oauth_ropc.pot new file mode 100644 index 0000000000..158c0b876e --- /dev/null +++ b/auth_oauth_ropc/i18n/auth_oauth_ropc.pot @@ -0,0 +1,107 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_oauth_ropc +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__active +msgid "Active" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__auth_endpoint +msgid "Authorization URL" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__client_id +msgid "Client ID" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__client_secret +msgid "Client Secret" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__create_uid +msgid "Created by" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__create_date +msgid "Created on" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__display_name +msgid "Display Name" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__id +msgid "ID" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider____last_update +msgid "Last Modified on" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__write_date +msgid "Last Updated on" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__name +msgid "Name" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model,name:auth_oauth_ropc.model_oauth_ropc_provider +msgid "OAuth ROPC Provider" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__resource +msgid "Resource" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model.fields,field_description:auth_oauth_ropc.field_oauth_ropc_provider__scope +msgid "Scope" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.model,name:auth_oauth_ropc.model_res_users +msgid "User" +msgstr "" + +#. module: auth_oauth_ropc +#. odoo-python +#: code:addons/auth_oauth_ropc/models/oauth_ropc_provider.py:0 +#, python-format +msgid "You can define only one active provider" +msgstr "" + +#. module: auth_oauth_ropc +#: model:ir.actions.act_window,name:auth_oauth_ropc.oauth_ropc_provider_act_window +#: model:ir.ui.menu,name:auth_oauth_ropc.oauth_ropc_provider_menu +msgid "oauth ROPC Providers" +msgstr "" From 83060f7fa2bbc1762c5b27a0ba53ff604fca73a7 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 19 Dec 2023 09:54:49 +0000 Subject: [PATCH 007/112] [BOT] post-merge updates --- README.md | 1 + auth_oauth_ropc/README.rst | 18 ++++---- auth_oauth_ropc/static/description/index.html | 43 ++++++++++--------- setup/_metapackage/VERSION.txt | 2 +- setup/_metapackage/setup.py | 1 + 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index e0cb4ba365..0a41fe8a15 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ addon | version | maintainers | summary [auth_jwt_demo](auth_jwt_demo/) | 16.0.1.1.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Test/demo module for auth_jwt. [auth_jwt_server_env](auth_jwt_server_env/) | 16.0.1.0.0 | | This addon adds auth.jwt.validator fields to server env [auth_ldaps](auth_ldaps/) | 16.0.1.0.0 | | Allows to use LDAP over SSL authentication +[auth_oauth_ropc](auth_oauth_ropc/) | 16.0.1.0.0 | | Allow to login with OAuth Resource Owner Password Credentials Grant [auth_oidc](auth_oidc/) | 16.0.1.0.2 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Allow users to login through OpenID Connect Provider [auth_oidc_environment](auth_oidc_environment/) | 16.0.1.0.0 | | This module allows to use server env for OIDC configuration [auth_saml](auth_saml/) | 16.0.1.0.3 | [![vincent-hatakeyama](https://github.com/vincent-hatakeyama.png?size=30px)](https://github.com/vincent-hatakeyama) | SAML2 Authentication diff --git a/auth_oauth_ropc/README.rst b/auth_oauth_ropc/README.rst index 63dab04b84..a99ac0f4f8 100644 --- a/auth_oauth_ropc/README.rst +++ b/auth_oauth_ropc/README.rst @@ -2,10 +2,13 @@ Auth OAuth ROPC =============== -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4a0d8a58b581d5e0b655aa88c5623aa0884cf6e0efd31437d5b2c506729fb85a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -19,21 +22,20 @@ Auth OAuth ROPC .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png :target: https://translation.odoo-community.org/projects/server-auth-16-0/server-auth-16-0-auth_oauth_ropc :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/251/16.0 - :alt: Try me on Runbot +.. |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=16.0 + :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| This module add the possibility to login with OAuth Resource Owner Password Credentials Grant -https://datatracker.ietf.org/doc/html/rfc6749#section-4.3 +https://datatracker.ietf.org/doc/html/rfc6749#section-4.3 In most scenarios, more secure alternatives are available and recommended. This flow requires a very high degree of trust in the application, and carries risks that are not present in other flows. You should only use this flow when other more secure flows aren't viable. This module is useful for the Odoo mobile application, which only supports user/password authentication. - **Table of contents** .. contents:: @@ -68,7 +70,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us smashing it by providing a detailed and welcomed +If you spotted it first, help us to smash it by providing a detailed and welcomed `feedback `_. Do not contact contributors directly about support or help with technical issues. diff --git a/auth_oauth_ropc/static/description/index.html b/auth_oauth_ropc/static/description/index.html index a100600ffa..040d6ff7a6 100644 --- a/auth_oauth_ropc/static/description/index.html +++ b/auth_oauth_ropc/static/description/index.html @@ -1,20 +1,19 @@ - - + Auth OAuth ROPC + + +
+

Vault

+ + +

Beta License: AGPL-3 OCA/server-auth Translate me on Weblate Try me on Runbot

+

This module implements a vault for secrets and files using end-to-end-encryption. The encryption and decryption happens in the browser using a vault specific shared master key. The master keys are encrypted using asymmetrically. For this the user has to enter a second password on the first login or if he needs to access data in a vault. The asymmetric keys are stored for a certain time in the browser storage.

+

The server can never access the secrets with the information available. Only people registered in the vault can decrypt or encrypt values in a vault. The meta data isn’t encrypted to be able to search/filter for entries more easily.

+

Table of contents

+ +
+

Known issues / Roadmap

+
    +
  • Field and file history for restoration
  • +
  • Import improvement
  • +
+
+
    +
  • Support challenge-response/FIDO2
  • +
  • Support for argon2 and kdbx v4
  • +
+
+
    +
  • Properly handle missing Crypto API because no https
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • initOS GmbH
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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 project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/vault_share/README.rst b/vault_share/README.rst new file mode 100644 index 0000000000..79c29aa762 --- /dev/null +++ b/vault_share/README.rst @@ -0,0 +1,78 @@ +============= +Vault - Share +============= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/14.0/vault_share + :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-14-0/server-auth-14-0-vault_share + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/251/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module implements possibilities to share specific secrets with external users. This bases on the vault implementation and the generated RSA key pair. + +Share +===== + +This allows an user to share a secret with external users. A share can be generated from a vault entry or directly created by an user. The secret is symmetrically encrypted by a key derived from a pin. To grant access the user has to transmit the link and pin with the external. If either the access counter reaches 0 or the share expires it will be deleted automatically. Due to the usage of a numeric pin and the browser side decryption a share is vulnerable to brute-force attacks and shouldn't be used as a permanent storage for secrets. For long time uses the user should create an account and a vault should be used. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* initOS GmbH + +Contributors +~~~~~~~~~~~~ + +* Florian Kantelberg + +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 `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/vault_share/static/description/index.html b/vault_share/static/description/index.html new file mode 100644 index 0000000000..915b781b46 --- /dev/null +++ b/vault_share/static/description/index.html @@ -0,0 +1,412 @@ + + + + + + +Vault - Share + + + +
+

Vault - Share

+ + +

Beta License: AGPL-3 OCA/server-auth Translate me on Weblate Try me on Runbot

+

This module implements possibilities to share specific secrets with external users. This bases on the vault implementation and the generated RSA key pair.

+
+

Share

+

This allows an user to share a secret with external users. A share can be generated from a vault entry or directly created by an user. The secret is symmetrically encrypted by a key derived from a pin. To grant access the user has to transmit the link and pin with the external. If either the access counter reaches 0 or the share expires it will be deleted automatically. Due to the usage of a numeric pin and the browser side decryption a share is vulnerable to brute-force attacks and shouldn’t be used as a permanent storage for secrets. For long time uses the user should create an account and a vault should be used.

+

Table of contents

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • initOS GmbH
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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 project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + From a4735994cd9eb7479daca34e32fc898a94ad9636 Mon Sep 17 00:00:00 2001 From: aromera Date: Thu, 30 Sep 2021 12:33:36 +0200 Subject: [PATCH 045/112] FIX vault: - Wrong field name vault_controller --- vault/controllers/main.py | 2 +- vault/static/src/js/vault_controller.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vault/controllers/main.py b/vault/controllers/main.py index 3338ad5426..6228c22a47 100644 --- a/vault/controllers/main.py +++ b/vault/controllers/main.py @@ -73,7 +73,7 @@ def vault_public(self, user_id): if not user or not user.keys: return {} - return {"public_key": user.active_keys.public} + return {"public_key": user.active_key.public} @http.route("/vault/keys/store", auth="user", type="json") def vault_store_keys(self, **kwargs): diff --git a/vault/static/src/js/vault_controller.js b/vault/static/src/js/vault_controller.js index 4df1dc520e..2b78c14674 100644 --- a/vault/static/src/js/vault_controller.js +++ b/vault/static/src/js/vault_controller.js @@ -284,7 +284,7 @@ odoo.define("vault.controller", function (require) { ), { confirm_callback: async function () { - await this._deleteVaultRight( + await self._deleteVaultRight( record, changes.right_ids, options From 4807043f397cd81d4a19507a51228c667d76879f Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 5 Oct 2021 13:35:28 +0000 Subject: [PATCH 046/112] vault 14.0.1.5.1 --- vault/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vault/__manifest__.py b/vault/__manifest__.py index 9154b86381..d2cfa8f47b 100644 --- a/vault/__manifest__.py +++ b/vault/__manifest__.py @@ -5,7 +5,7 @@ "name": "Vault", "summary": "Password vault integration in Odoo", "license": "AGPL-3", - "version": "14.0.1.5.0", + "version": "14.0.1.5.1", "website": "https://github.com/OCA/server-auth", "application": True, "author": "initOS GmbH, Odoo Community Association (OCA)", From eb897864c3e1b1f309da0f76cc883b5cb0a472f6 Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Wed, 27 Oct 2021 18:32:33 +0200 Subject: [PATCH 047/112] General fixes and improvements of the vault - Send wizard couldn't select all possible users due to access rights - Disable the features if browser isn't using a secure context (https/localhost) - Load required assets in inbox and share frontends - Allow the user to switch a parent of an entry within the same vault - Allow to create entries for vaults using the main menu Entries - Fix bug that encrypted a share twice due to multiple extension with VaultAbstract - Fix a bug on share create that throws a warning about changed values --- vault/__manifest__.py | 3 +- vault/controllers/main.py | 12 +- vault/i18n/vault.pot | 10 + vault/models/__init__.py | 2 + vault/models/abstract_vault.py | 12 +- vault/models/abstract_vault_field.py | 1 + vault/models/res_config_settings.py | 10 + vault/models/res_users.py | 4 +- vault/models/vault.py | 6 + vault/models/vault_entry.py | 32 +++- vault/models/vault_inbox.py | 34 +++- vault/models/vault_inbox_log.py | 22 +++ vault/models/vault_right.py | 18 +- vault/models/vault_tag.py | 2 +- vault/readme/DESCRIPTION.rst | 2 + vault/readme/ROADMAP.rst | 4 +- vault/security/ir.model.access.csv | 1 + vault/security/ir_rule.xml | 23 +++ vault/static/src/js/vault.js | 2 + vault/static/src/js/vault_controller.js | 4 + vault/static/src/js/vault_export.js | 2 + vault/static/src/js/vault_import.js | 2 + vault/static/src/js/vault_inbox.js | 9 + vault/static/src/js/vault_utils.js | 13 +- vault/static/src/js/vault_widget.js | 43 +++-- vault/static/src/xml/templates.xml | 22 ++- vault/tests/__init__.py | 9 +- vault/tests/test_controller.py | 171 ++++++++++++++++++ vault/tests/test_inbox.py | 4 +- vault/tests/test_rights.py | 14 ++ vault/tests/test_user.py | 9 +- vault/tests/test_vault.py | 26 +++ vault/views/assets.xml | 1 + vault/views/menuitems.xml | 12 +- vault/views/res_config_settings_views.xml | 26 +++ vault/views/res_users_views.xml | 2 +- vault/views/templates.xml | 2 + vault/views/vault_entry_views.xml | 52 ++++-- vault/views/vault_inbox_views.xml | 11 +- vault/views/vault_right_views.xml | 7 +- vault/views/vault_views.xml | 1 + vault/wizards/vault_send_wizard.py | 25 +-- vault/wizards/vault_send_wizard.xml | 5 +- vault/wizards/vault_store_wizard.xml | 4 +- vault_share/__manifest__.py | 5 +- vault_share/controllers/main.py | 3 +- vault_share/models/__init__.py | 2 +- vault_share/models/res_company.py | 10 + vault_share/models/res_config_settings.py | 24 +++ vault_share/models/vault_share.py | 16 +- vault_share/models/vault_share_log.py | 22 +++ vault_share/security/ir.model.access.csv | 1 + vault_share/security/ir_rule.xml | 10 + vault_share/static/src/js/vault_share.js | 4 +- .../static/src/js/vault_share_widget.js | 16 +- vault_share/static/src/xml/templates.xml | 15 +- vault_share/tests/test_share.py | 63 +++++-- vault_share/views/assets.xml | 1 + vault_share/views/menuitems.xml | 2 +- .../views/res_config_settings_views.xml | 18 ++ vault_share/views/vault_share_views.xml | 11 +- 61 files changed, 786 insertions(+), 113 deletions(-) create mode 100644 vault/models/res_config_settings.py create mode 100644 vault/models/vault_inbox_log.py create mode 100644 vault/tests/test_controller.py create mode 100644 vault/views/res_config_settings_views.xml create mode 100644 vault_share/models/res_company.py create mode 100644 vault_share/models/res_config_settings.py create mode 100644 vault_share/models/vault_share_log.py create mode 100644 vault_share/views/res_config_settings_views.xml diff --git a/vault/__manifest__.py b/vault/__manifest__.py index d2cfa8f47b..1d823cb790 100644 --- a/vault/__manifest__.py +++ b/vault/__manifest__.py @@ -5,7 +5,7 @@ "name": "Vault", "summary": "Password vault integration in Odoo", "license": "AGPL-3", - "version": "14.0.1.5.1", + "version": "14.0.1.6.0", "website": "https://github.com/OCA/server-auth", "application": True, "author": "initOS GmbH, Odoo Community Association (OCA)", @@ -15,6 +15,7 @@ "security/ir.model.access.csv", "security/ir_rule.xml", "views/assets.xml", + "views/res_config_settings_views.xml", "views/res_users_views.xml", "views/vault_entry_views.xml", "views/vault_field_views.xml", diff --git a/vault/controllers/main.py b/vault/controllers/main.py index 6228c22a47..2c170bb9b9 100644 --- a/vault/controllers/main.py +++ b/vault/controllers/main.py @@ -16,7 +16,6 @@ def vault_inbox(self, token): # Find the right token inbox = request.env["vault.inbox"].sudo().find_inbox(token) user = request.env["res.users"].sudo().find_user_of_inbox(token) - _logger.info("%s: %s", inbox, user) if len(inbox) == 1 and inbox.accesses > 0: ctx.update({"name": inbox.name, "public": inbox.user_id.active_key.public}) elif len(inbox) == 0 and len(user) == 1: @@ -55,7 +54,16 @@ def vault_inbox(self, token): return request.render("vault.inbox", ctx) try: - inbox.store_in_inbox(name, secret, secret_file, iv, key, user, filename) + inbox.store_in_inbox( + name, + secret, + secret_file, + iv, + key, + user, + filename, + ip=request.httprequest.remote_addr, + ) except Exception as e: _logger.exception(e) ctx["error"] = _( diff --git a/vault/i18n/vault.pot b/vault/i18n/vault.pot index 4abdf1abe2..6e8292a62d 100644 --- a/vault/i18n/vault.pot +++ b/vault/i18n/vault.pot @@ -99,6 +99,16 @@ msgstr "" msgid "Allowed Write" msgstr "" +#. module: vault +#: model:ir.model.fields,field_description:vault.field_vault__allowed_create +#: model:ir.model.fields,field_description:vault.field_vault_abstract_field__allowed_create +#: model:ir.model.fields,field_description:vault.field_vault_entry__allowed_create +#: model:ir.model.fields,field_description:vault.field_vault_field__allowed_create +#: model:ir.model.fields,field_description:vault.field_vault_file__allowed_create +#: model:ir.model.fields,field_description:vault.field_vault_right__allowed_create +msgid "Allowed Create" +msgstr "" + #. module: vault #: code:addons/vault/controllers/main.py:0 #, python-format diff --git a/vault/models/__init__.py b/vault/models/__init__.py index 492cc527a4..d9d3e40ebe 100644 --- a/vault/models/__init__.py +++ b/vault/models/__init__.py @@ -4,6 +4,7 @@ from . import ( abstract_vault, abstract_vault_field, + res_config_settings, res_users, res_users_key, vault, @@ -11,6 +12,7 @@ vault_field, vault_file, vault_inbox, + vault_inbox_log, vault_log, vault_right, vault_tag, diff --git a/vault/models/abstract_vault.py b/vault/models/abstract_vault.py index 4e63e10b93..614173056d 100644 --- a/vault/models/abstract_vault.py +++ b/vault/models/abstract_vault.py @@ -12,7 +12,8 @@ class AbstractVault(models.AbstractModel): """Models must have the following fields: `perm_user`: The permissions are computed for this user - `allowed_write`: The current user can read from the vault + `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 @@ -41,10 +42,19 @@ def check_access_rule(self, operation): 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() diff --git a/vault/models/abstract_vault_field.py b/vault/models/abstract_vault_field.py index bb062c90a7..d819a32f91 100644 --- a/vault/models/abstract_vault_field.py +++ b/vault/models/abstract_vault_field.py @@ -18,6 +18,7 @@ class AbstractVaultField(models.AbstractModel): 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) diff --git a/vault/models/res_config_settings.py b/vault/models/res_config_settings.py new file mode 100644 index 0000000000..00923c6c42 --- /dev/null +++ b/vault/models/res_config_settings.py @@ -0,0 +1,10 @@ +# © 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() diff --git a/vault/models/res_users.py b/vault/models/res_users.py index 1c836c1914..11e52ecb5a 100644 --- a/vault/models/res_users.py +++ b/vault/models/res_users.py @@ -39,9 +39,9 @@ def _compute_inbox_link(self): def action_get_vault(self): return self.sudo().env.ref("vault.action_res_users_keys").read()[0] - def action_new_share_token(self): + def action_new_inbox_token(self): self.ensure_one() - self.inbox_token = uuid4() + self.sudo().inbox_token = uuid4() return {"type": "ir.actions.act_window_close"} @api.model diff --git a/vault/models/vault.py b/vault/models/vault.py index 873d10fe42..da6293792d 100644 --- a/vault/models/vault.py +++ b/vault/models/vault.py @@ -36,6 +36,7 @@ class Vault(models.Model): # 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) @@ -63,6 +64,7 @@ def _compute_access(self): if user == rec.user_id: rec.write( { + "allowed_create": True, "allowed_share": True, "allowed_write": True, "allowed_delete": True, @@ -73,6 +75,9 @@ def _compute_access(self): 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( @@ -100,6 +105,7 @@ def _get_default_rights(self): 0, { "user_id": self.env.uid, + "perm_create": True, "perm_write": True, "perm_delete": True, "perm_share": True, diff --git a/vault/models/vault_entry.py b/vault/models/vault_entry.py index 1e829fe6e4..75efd376a9 100644 --- a/vault/models/vault_entry.py +++ b/vault/models/vault_entry.py @@ -6,6 +6,7 @@ from uuid import uuid4 from odoo import _, api, fields, models +from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) @@ -17,7 +18,12 @@ class VaultEntry(models.Model): _order = "complete_name" _rec_name = "complete_name" - parent_id = fields.Many2one("vault.entry", "Parent", ondelete="cascade") + 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) @@ -28,6 +34,7 @@ class VaultEntry(models.Model): 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) @@ -43,12 +50,21 @@ class VaultEntry(models.Model): note = fields.Text() tags = fields.Many2many("vault.tag") expire_date = fields.Datetime("Expires on", default=False) - expired = fields.Boolean(compute="_compute_expired", store=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: @@ -63,10 +79,20 @@ def _compute_expired(self): 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): self.ensure_one() self.log_info( - f"{action} entry {self.complete_name} by {self.env.user.display_name}" + _("%s entry %s by %s") + % (action, self.complete_name, self.env.user.display_name) ) @api.model_create_single diff --git a/vault/models/vault_inbox.py b/vault/models/vault_inbox.py index c2471bfeb4..f4687d6e7a 100644 --- a/vault/models/vault_inbox.py +++ b/vault/models/vault_inbox.py @@ -14,7 +14,7 @@ class VaultInbox(models.Model): _name = "vault.inbox" _description = _("Vault share incoming secrets") - token = fields.Char(default=lambda self: uuid4(), readonly=True) + token = fields.Char(default=lambda self: uuid4(), readonly=True, copy=False) inbox_link = fields.Char( compute="_compute_inbox_link", readonly=True, @@ -22,7 +22,11 @@ class VaultInbox(models.Model): "to create new inboxes you should give them your inbox link from your key " "management.", ) - user_id = fields.Many2one("res.users", "Vault", required=True) + user_id = fields.Many2one( + "res.users", + "Vault", + required=True, + ) name = fields.Char(required=True) secret = fields.Char(readonly=True) filename = fields.Char() @@ -32,12 +36,13 @@ class VaultInbox(models.Model): accesses = fields.Integer( "Access counter", default=1, - help="If this is 0 the inbox will be read-only for the owner.", + 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 will be read-only for the owner.", + 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 = [ ( @@ -63,8 +68,19 @@ def read(self, *args, **kwargs): def find_inbox(self, token): return self.search([("token", "=", token)]) - def store_in_inbox(self, name, secret, secret_file, iv, key, user, filename): + def store_in_inbox( + self, + name, + secret, + secret_file, + iv, + key, + user, + filename, + ip=None, + ): if len(self) == 0: + log = _("Created by %s via %s") % (user.name, ip or "n/a") return self.create( { "name": name, @@ -75,10 +91,17 @@ def store_in_inbox(self, name, secret, secret_file, iv, key, user, filename): "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 %s via %s") % ( + self.env.user.name, + ip or "n/a", + ) + self.write( { "accesses": self.accesses - 1, @@ -87,6 +110,7 @@ def store_in_inbox(self, name, secret, secret_file, iv, key, user, filename): "secret": secret or None, "secret_file": secret_file or None, "filename": filename, + "log_ids": [(0, 0, {"name": log})], } ) return self diff --git a/vault/models/vault_inbox_log.py b/vault/models/vault_inbox_log.py new file mode 100644 index 0000000000..e072283624 --- /dev/null +++ b/vault/models/vault_inbox_log.py @@ -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) diff --git a/vault/models/vault_right.py b/vault/models/vault_right.py index a4aa6caca2..20b1684814 100644 --- a/vault/models/vault_right.py +++ b/vault/models/vault_right.py @@ -19,9 +19,17 @@ class VaultRight(models.Model): ) 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 + "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(), @@ -40,6 +48,7 @@ class VaultRight(models.Model): 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) @@ -48,7 +57,7 @@ class VaultRight(models.Model): key = fields.Char() _sql_constraints = ( - ("user_uniq", "UNIQUE(user_id, vault_id)", "The user must be unique"), + ("user_uniq", "UNIQUE(user_id, vault_id)", _("The user must be unique")), ) def _get_is_owner(self): @@ -66,7 +75,7 @@ def log_access(self): ["read"] + [ right - for right in ["write", "share", "delete"] + for right in ["create", "write", "share", "delete"] if getattr(self, f"perm_{right}", False) ] ) @@ -87,7 +96,8 @@ def create(self, values): def write(self, values): res = super().write(values) - if any(x in values for x in ["perm_write", "perm_delete", "perm_share"]): + perms = ["perm_write", "perm_delete", "perm_share", "perm_create"] + if any(x in values for x in perms): for rec in self: rec.log_access() diff --git a/vault/models/vault_tag.py b/vault/models/vault_tag.py index 12aa3e6cb2..f50ae6de88 100644 --- a/vault/models/vault_tag.py +++ b/vault/models/vault_tag.py @@ -12,5 +12,5 @@ class VaultTag(models.Model): name = fields.Char(required=True) _sql_constraints = [ - ("name_uniq", "unique(name)", "The tag must be unique!"), + ("name_uniq", "unique(name)", _("The tag must be unique!")), ] diff --git a/vault/readme/DESCRIPTION.rst b/vault/readme/DESCRIPTION.rst index ffccef96fe..3f156370e8 100644 --- a/vault/readme/DESCRIPTION.rst +++ b/vault/readme/DESCRIPTION.rst @@ -1,3 +1,5 @@ This module implements a vault for secrets and files using end-to-end-encryption. The encryption and decryption happens in the browser using a vault specific shared master key. The master keys are encrypted using asymmetrically. For this the user has to enter a second password on the first login or if he needs to access data in a vault. The asymmetric keys are stored for a certain time in the browser storage. The server can never access the secrets with the information available. Only people registered in the vault can decrypt or encrypt values in a vault. The meta data isn't encrypted to be able to search/filter for entries more easily. + +This modules requires a secure context for the browser to work properly. diff --git a/vault/readme/ROADMAP.rst b/vault/readme/ROADMAP.rst index bf394cbda4..63124da1f6 100644 --- a/vault/readme/ROADMAP.rst +++ b/vault/readme/ROADMAP.rst @@ -1,8 +1,8 @@ * Field and file history for restoration +* Send secrets directly to an inbox within Odoo + * Import improvement * Support challenge-response/FIDO2 * Support for argon2 and kdbx v4 - -* Properly handle missing Crypto API because no https diff --git a/vault/security/ir.model.access.csv b/vault/security/ir.model.access.csv index 3d7db5d0b2..0863b98aa8 100644 --- a/vault/security/ir.model.access.csv +++ b/vault/security/ir.model.access.csv @@ -7,6 +7,7 @@ access_vault_file,access_vault_file,model_vault_file,base.group_user,1,1,1,1 access_vault_import_wizard,access_vault_import_wizard,model_vault_import_wizard,base.group_user,1,1,1,1 access_vault_import_wizard_path,access_vault_import_wizard_path,model_vault_import_wizard_path,base.group_user,1,1,1,1 access_vault_inbox,access_vault_inbox,model_vault_inbox,base.group_user,1,1,1,1 +access_vault_inbox_log,access_vault_inbox_log,model_vault_inbox_log,base.group_user,1,1,1,1 access_vault_log,access_vault_log,model_vault_log,base.group_user,1,0,0,0 access_vault_right,access_vault_right,model_vault_right,base.group_user,1,1,1,1 access_vault_send_wizard,access_vault_send_wizard,model_vault_send_wizard,base.group_user,1,1,1,1 diff --git a/vault/security/ir_rule.xml b/vault/security/ir_rule.xml index 25707ba7b9..13f7cdb45f 100644 --- a/vault/security/ir_rule.xml +++ b/vault/security/ir_rule.xml @@ -85,4 +85,27 @@ + + + vault.right.access.default + + [('vault_id.right_ids.user_id', '=', user.id)] + + + + + + + + + vault.inbox.log.access.owner + + [('inbox_id.user_id', '=', user.id)] + + + + + diff --git a/vault/static/src/js/vault.js b/vault/static/src/js/vault.js index 70617a7548..2c4e16da40 100644 --- a/vault/static/src/js/vault.js +++ b/vault/static/src/js/vault.js @@ -57,6 +57,8 @@ odoo.define("vault", function (require) { var self = this; function waitAndCheck() { + if (!utils.supported()) return null; + if (odoo.isReady) self._initialize_keys(); else setTimeout(waitAndCheck, 500); } diff --git a/vault/static/src/js/vault_controller.js b/vault/static/src/js/vault_controller.js index 2b78c14674..a0b0ee96da 100644 --- a/vault/static/src/js/vault_controller.js +++ b/vault/static/src/js/vault_controller.js @@ -103,6 +103,8 @@ odoo.define("vault.controller", function (require) { */ _onGenerateKeys: async function (ev) { ev.stopPropagation(); + if (!utils.supported()) return; + var self = this; Dialog.confirm( @@ -334,6 +336,8 @@ odoo.define("vault.controller", function (require) { _applyChanges: async function (dataPointID, changes, options) { const result = await this._super.apply(this, arguments); + if (!utils.supported()) return result; + const record = this.model.get(dataPointID); if (record.model === "vault") await this._applyChangesVault(record, changes, options); diff --git a/vault/static/src/js/vault_export.js b/vault/static/src/js/vault_export.js index 79f2f3885c..d74e2a6e31 100644 --- a/vault/static/src/js/vault_export.js +++ b/vault/static/src/js/vault_export.js @@ -124,6 +124,8 @@ odoo.define("vault.export", function (require) { * @returns the data importable by the backend or false on error */ export: async function (master_key, filename, content) { + if (!utils.supported()) return false; + if (filename.endsWith(".json")) return await this._export_json(master_key, content); return false; diff --git a/vault/static/src/js/vault_import.js b/vault/static/src/js/vault_import.js index 5cdd8ee302..29b5ae1aba 100644 --- a/vault/static/src/js/vault_import.js +++ b/vault/static/src/js/vault_import.js @@ -247,6 +247,8 @@ odoo.define("vault.import", function (require) { * @returns the data importable by the backend or false on error */ import: async function (master_key, filename, content) { + if (!utils.supported()) return false; + if (filename.endsWith(".json")) return await this._import_json(master_key, content); else if (filename.endsWith(".kdbx")) diff --git a/vault/static/src/js/vault_inbox.js b/vault/static/src/js/vault_inbox.js index 37ed894f96..5f4e757812 100644 --- a/vault/static/src/js/vault_inbox.js +++ b/vault/static/src/js/vault_inbox.js @@ -20,6 +20,7 @@ odoo.define("vault.inbox", function (require) { "encrypted_file", "filename", "secret_file", + "submit", ]; function toggle_required(element, value) { @@ -29,6 +30,8 @@ odoo.define("vault.inbox", function (require) { // Encrypt the value and store it in the right input field async function encrypt_and_store(value, target) { + if (!utils.supported()) return false; + // Find all the possible elements which are needed for (const id of fields) if (!data[id]) data[id] = document.getElementById(id); @@ -56,14 +59,19 @@ odoo.define("vault.inbox", function (require) { } document.getElementById("secret").onchange = async function () { + if (!utils.supported()) return false; + if (!this.value) return; const required = await encrypt_and_store(this.value, "encrypted"); toggle_required(data.secret, required); toggle_required(data.secret_file, !required); + data.submit.removeAttribute("disabled"); }; document.getElementById("secret_file").onchange = async function () { + if (!utils.supported()) return false; + if (!this.files.length) return; const file = this.files[0]; @@ -74,5 +82,6 @@ odoo.define("vault.inbox", function (require) { toggle_required(data.secret, !required); toggle_required(data.secret_file, required); data.filename.value = file.name; + data.submit.removeAttribute("disabled"); }; }); diff --git a/vault/static/src/js/vault_utils.js b/vault/static/src/js/vault_utils.js index 7f69772c79..e88fc38c64 100644 --- a/vault/static/src/js/vault_utils.js +++ b/vault/static/src/js/vault_utils.js @@ -1,8 +1,6 @@ // © 2021 Florian Kantelberg - initOS GmbH // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -/* global Uint8Array */ - odoo.define("vault.utils", function (require) { "use strict"; @@ -34,6 +32,15 @@ odoo.define("vault.utils", function (require) { length: 256, }; + /** + * Checks if the CryptoAPI is available and the vault feature supported + * + * @returns if vault is supported + */ + function supported() { + return Boolean(CryptoAPI); + } + /** * Converts an ArrayBuffer to an ASCII string * @@ -573,5 +580,7 @@ odoo.define("vault.utils", function (require) { fromBinary: fromBinary, toBase64: toBase64, toBinary: toBinary, + + supported: supported, }; }); diff --git a/vault/static/src/js/vault_widget.js b/vault/static/src/js/vault_widget.js index 5ef07b0429..011e57373e 100644 --- a/vault/static/src/js/vault_widget.js +++ b/vault/static/src/js/vault_widget.js @@ -1,8 +1,6 @@ // © 2021 Florian Kantelberg - initOS GmbH // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -/* global ArrayBuffer, Uint8Array */ - odoo.define("vault.fields", function (require) { "use strict"; @@ -18,6 +16,9 @@ odoo.define("vault.fields", function (require) { var QWeb = core.qweb; var VaultAbstract = { + supported: function () { + return utils.supported(); + }, /** * Set the value by encrypting it * @@ -28,9 +29,12 @@ odoo.define("vault.fields", function (require) { _setValue: function (value, options) { const self = this; const _super = this._super; - return this._encrypt(value).then(function (data) { - _super.call(self, data, options); - }); + + if (utils.supported()) { + return this._encrypt(value).then(function (data) { + _super.call(self, data, options); + }); + } }, /** @@ -55,6 +59,8 @@ odoo.define("vault.fields", function (require) { * @returns the IV to use */ _getIV: function () { + if (!utils.supported()) return null; + // IV already read. Reuse it if (this.iv) return this.iv; @@ -74,6 +80,8 @@ odoo.define("vault.fields", function (require) { * @returns the master key to use */ _getMasterKey: async function () { + if (!utils.supported()) return null; + // Check if the master key is already extracted if (this.key) return await vault.unwrap(this.key); @@ -180,6 +188,8 @@ odoo.define("vault.fields", function (require) { * @returns the decrypted data */ _decrypt: async function (data) { + if (!utils.supported()) return null; + const iv = this._getIV(); const key = await this._getMasterKey(); return await utils.sym_decrypt(key, data, iv); @@ -192,6 +202,8 @@ odoo.define("vault.fields", function (require) { * @returns the encrypted data */ _encrypt: async function (data) { + if (!utils.supported()) return null; + const iv = this._getIV(); const key = await this._getMasterKey(); return await utils.sym_encrypt(key, data, iv); @@ -266,6 +278,7 @@ odoo.define("vault.fields", function (require) { * @param {String} value to render */ _renderValue: function (value) { + const self = this; this.$el.html( QWeb.render(this.template, { widget: self, @@ -292,9 +305,11 @@ odoo.define("vault.fields", function (require) { this.$input.addClass("o_input"); this.$input.attr(inputAttrs); - this._decrypt(this.value).then(function (data) { - self.$input.val(self._formatValue(data)); - }); + if (utils.supported()) { + this._decrypt(this.value).then(function (data) { + self.$input.val(self._formatValue(data)); + }); + } return this.$input; }, @@ -337,7 +352,7 @@ odoo.define("vault.fields", function (require) { _t("The field is empty, there's nothing to save!") ); ev.stopPropagation(); - } else if (this.res_id) { + } else if (this.res_id && utils.supported()) { ev.stopPropagation(); const filename_fieldname = this.attrs.filename; @@ -397,7 +412,7 @@ odoo.define("vault.fields", function (require) { * @param {OdooEvent} ev */ on_save_as: async function (ev) { - if (this.value) { + if (this.value && utils.supported()) { ev.stopPropagation(); const exporter = new Exporter(); @@ -424,7 +439,7 @@ odoo.define("vault.fields", function (require) { }, }); - var VaultInboxField = VaultField.extend(VaultAbstract, { + var VaultInboxField = VaultField.extend({ store_model: "vault.field", events: _.extend({}, VaultField.prototype.events, { "click .o_vault_show": "_onShowValue", @@ -454,6 +469,8 @@ odoo.define("vault.fields", function (require) { * @returns the decrypted data */ _decrypt: async function (data) { + if (!utils.supported()) return null; + const iv = this.recordData[this.field_iv]; const wrapped_key = this.recordData[this.field_key]; @@ -474,7 +491,7 @@ odoo.define("vault.fields", function (require) { }); // Widget used to view shared incoming secrets encrypted with public keys - var VaultInboxFile = VaultFile.extend(VaultAbstract, { + var VaultInboxFile = VaultFile.extend({ store_model: "vault.file", template: "FileVaultInbox", events: _.extend({}, VaultFile.prototype.events, { @@ -517,6 +534,8 @@ odoo.define("vault.fields", function (require) { * @returns the decrypted data */ _decrypt: async function (data) { + if (!utils.supported()) return null; + const iv = this.recordData[this.field_iv]; const wrapped_key = this.recordData[this.field_key]; diff --git a/vault/static/src/xml/templates.xml b/vault/static/src/xml/templates.xml index f7cbeb3e7f..b6e8cb73b1 100644 --- a/vault/static/src/xml/templates.xml +++ b/vault/static/src/xml/templates.xml @@ -135,7 +135,10 @@ -
+
+ ******* +
+
diff --git a/vault/tests/__init__.py b/vault/tests/__init__.py index 9ceb22d222..59ea2f2ef1 100644 --- a/vault/tests/__init__.py +++ b/vault/tests/__init__.py @@ -1,4 +1,11 @@ # © 2021 Florian Kantelberg - initOS GmbH # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from . import test_log, test_rights, test_user, test_vault, test_widgets +from . import ( + test_controller, + test_log, + test_rights, + test_user, + test_vault, + test_widgets, +) diff --git a/vault/tests/test_controller.py b/vault/tests/test_controller.py new file mode 100644 index 0000000000..ea4c5e5eb5 --- /dev/null +++ b/vault/tests/test_controller.py @@ -0,0 +1,171 @@ +# © 2021 Florian Kantelberg - initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from unittest.mock import MagicMock, patch + +from odoo.tests import TransactionCase +from odoo.tools import mute_logger + +from ..controllers import main + +_logger = logging.getLogger(__name__) + + +@patch("odoo.addons.vault.controllers.main.request") +class TestController(TransactionCase): + def setUp(self): + super().setUp() + + self.controller = main.Controller() + + self.user = self.env["res.users"].create( + {"login": "test", "email": "test@test", "name": "test"} + ) + self.user.inbox_token = "42" + self.user.keys.current = False + self.key = self.env["res.users.key"].create( + { + "user_id": self.user.id, + "public": "a public key", + "salt": "42", + "iv": "2424", + "iterations": 4000, + "private": "24", + "current": True, + } + ) + self.inbox = self.env["vault.inbox"].create( + { + "user_id": self.user.id, + "name": "Inbox", + "key": "4", + "iv": "1", + "secret": "old secret", + "secret_file": "old file", + "accesses": 100, + } + ) + + patcher = patch("odoo.http.request") + self.addCleanup(patcher.stop) + patcher.start() + + @mute_logger("odoo.addons.vault.controllers.main") + @mute_logger("odoo.sql_db") + def test_vault_inbox(self, request_mock): + def return_context(template, context): + self.assertEqual(template, "vault.inbox") + return context + + request_mock.env = self.env + request_mock.render.side_effect = return_context + response = self.controller.vault_inbox("") + self.assertIn("error", response) + + response = self.controller.vault_inbox(self.user.inbox_token) + self.assertNotIn("error", response) + self.assertEqual(response["public"], self.user.active_key.public) + + # Try to eliminate each error step by step + request_mock.httprequest.method = "POST" + request_mock.params = {} + response = self.controller.vault_inbox(self.user.inbox_token) + self.assertIn("error", response) + + request_mock.params["name"] = "test" + response = self.controller.vault_inbox(self.user.inbox_token) + self.assertIn("error", response) + + request_mock.params.update({"encrypted": "secret", "encrypted_file": "file"}) + response = self.controller.vault_inbox(self.user.inbox_token) + self.assertIn("error", response) + + request_mock.params["filename"] = "filename" + response = self.controller.vault_inbox(self.user.inbox_token) + self.assertIn("error", response) + + self.assertEqual(self.inbox.secret, "old secret") + self.assertEqual(self.inbox.secret_file, b"old file") + + # Store something successfully + request_mock.params.update({"iv": "iv", "key": "key"}) + response = self.controller.vault_inbox(self.inbox.token) + self.assertNotIn("error", response) + self.assertEqual(self.inbox.secret, "secret") + self.assertEqual(self.inbox.secret_file, b"file") + + # Test a duplicate inbox + self.inbox.copy().token = self.inbox.token + response = self.controller.vault_inbox(self.inbox.token) + self.assertIn("error", response) + + def raise_error(*args, **kwargs): + raise TypeError() + + # Catch internal errors + try: + request_mock.httprequest.remote_addr = "127.0.0.1" + self.env["vault.inbox"]._patch_method("store_in_inbox", raise_error) + response = self.controller.vault_inbox(self.user.inbox_token) + finally: + self.env["vault.inbox"]._revert_method("store_in_inbox") + + self.assertIn("error", response) + + @mute_logger("odoo.sql_db") + def test_vault_public(self, request_mock): + request_mock.env = self.env + no_key = self.env["res.users"].create( + {"login": "keyless", "email": "test@test", "name": "test"} + ) + + response = self.controller.vault_public(user_id=no_key.id) + self.assertEqual(response, {}) + + response = self.controller.vault_public(user_id=self.user.id) + self.assertEqual(response["public_key"], self.key.public) + + @mute_logger("odoo.sql_db") + def test_vault_store(self, request_mock): + request_mock.env = self.env + mock = MagicMock() + try: + self.env["res.users.key"]._patch_method("store", mock) + self.controller.vault_store_keys() + mock.assert_called_once() + finally: + self.env["res.users.key"]._revert_method("store") + + @mute_logger("odoo.sql_db") + def test_vault_keys_get(self, request_mock): + request_mock.env = self.env + mock = MagicMock() + try: + self.env["res.users"]._patch_method("get_vault_keys", mock) + self.controller.vault_get_keys() + mock.assert_called_once() + finally: + self.env["res.users"]._revert_method("get_vault_keys") + + @mute_logger("odoo.sql_db") + def test_vault_right_keys(self, request_mock): + request_mock.env = self.env + self.assertFalse(self.controller.vault_get_right_keys()) + + # New vault with user as owner and only right + vault = self.env["vault"].create({"name": "Vault"}) + + response = self.controller.vault_get_right_keys() + self.assertEqual(response, {vault.uuid: vault.right_ids.key}) + + @mute_logger("odoo.sql_db") + def test_vault_store_right_key(self, request_mock): + request_mock.env = self.env + + vault = self.env["vault"].create({"name": "Vault"}) + + self.controller.vault_store_right_keys(None) + + self.controller.vault_store_right_keys({vault.uuid: "new key"}) + self.assertEqual(vault.right_ids.key, "new key") diff --git a/vault/tests/test_inbox.py b/vault/tests/test_inbox.py index 017b4ca02d..e02402ba8e 100644 --- a/vault/tests/test_inbox.py +++ b/vault/tests/test_inbox.py @@ -16,7 +16,7 @@ def test_user_inbox(self): {"login": "test", "email": "test@test", "name": "test"} ) - user.action_new_share_token() + user.action_new_inbox_token() model = self.env["res.users"] token = user.inbox_token @@ -27,7 +27,7 @@ def test_user_inbox(self): user.inbox_enabled = False self.assertEqual(model, model.find_user_of_inbox(token)) - user.action_new_share_token() + user.action_new_inbox_token() self.assertNotEqual(user.inbox_token, token) def test_inbox(self): diff --git a/vault/tests/test_rights.py b/vault/tests/test_rights.py index 08d5de5acb..12f83423df 100644 --- a/vault/tests/test_rights.py +++ b/vault/tests/test_rights.py @@ -50,6 +50,19 @@ def test_owner_access(self): right.perm_delete = False obj.unlink() + def test_no_create(self): + self.env["vault.right"].create( + { + "vault_id": self.vault.id, + "user_id": self.user.id, + "perm_create": False, + } + ) + + for obj in [self.field, self.entry, self.vault]: + with self.assertRaises(AccessError): + obj.with_user(self.user).check_access_rule("create") + def test_no_right(self): # No right defined for test user means access denied for obj in [self.field, self.entry, self.vault]: @@ -68,6 +81,7 @@ def test_no_permission(self): { "vault_id": self.vault.id, "user_id": self.user.id, + "perm_create": False, "perm_write": False, "perm_delete": False, } diff --git a/vault/tests/test_user.py b/vault/tests/test_user.py index 051bb1b71b..bbddb1a0ec 100644 --- a/vault/tests/test_user.py +++ b/vault/tests/test_user.py @@ -14,7 +14,7 @@ def test_user_inbox(self): {"login": "test", "email": "test@test", "name": "test"} ) - user.action_new_share_token() + user.action_new_inbox_token() model = self.env["res.users"] token = user.inbox_token @@ -25,5 +25,10 @@ def test_user_inbox(self): user.inbox_enabled = False self.assertEqual(model, model.find_user_of_inbox(token)) - user.action_new_share_token() + user.action_new_inbox_token() self.assertNotEqual(user.inbox_token, token) + + def test_user_key_management(self): + action = self.env.ref("vault.action_res_users_keys") + + self.assertEqual(action.id, self.env["res.users"].action_get_vault()["id"]) diff --git a/vault/tests/test_vault.py b/vault/tests/test_vault.py index 5a46c3f2b3..58f5326e87 100644 --- a/vault/tests/test_vault.py +++ b/vault/tests/test_vault.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging +from datetime import datetime from odoo.exceptions import ValidationError from odoo.tests import TransactionCase @@ -129,3 +130,28 @@ def test_vault_keys(self): keys = self.env.user.get_vault_keys() for key in ["private", "public", "iv", "salt", "iterations"]: self.assertEqual(keys[key], data[key]) + + def test_vault_entry_recursion(self): + child = self.env["vault.entry"].create( + {"vault_id": self.vault.id, "name": "Entry", "parent_id": self.entry.id} + ) + + with self.assertRaises(ValidationError): + self.entry.parent_id = child.id + + def test_search_expired(self): + entry = self.env["vault.entry"] + self.assertEqual(entry._search_expired("in", []), []) + + domain = entry._search_expired("=", True) + self.assertEqual(domain[0][:2], ("expire_date", "<")) + self.assertIsInstance(domain[0][2], datetime) + + domain = entry._search_expired("!=", False) + self.assertEqual(domain[0][:2], ("expire_date", "<")) + self.assertIsInstance(domain[0][2], datetime) + + domain = entry._search_expired("=", False) + self.assertTrue(domain[0] == "|") + self.assertIn(("expire_date", "=", False), domain) + self.assertTrue(any(("expire_date", ">=") == d[:2] for d in domain)) diff --git a/vault/views/assets.xml b/vault/views/assets.xml index c44b96a0e2..cff9055e31 100644 --- a/vault/views/assets.xml +++ b/vault/views/assets.xml @@ -31,6 +31,7 @@