Skip to content

Commit 4c2e85b

Browse files
committed
[19.0][IMP] [IMP] auth_api_key: generate API keys automatically
1 parent 957944c commit 4c2e85b

6 files changed

Lines changed: 75 additions & 2 deletions

File tree

auth_api_key/README.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ The api key menu is available into Settings > Technical in debug mode.
7070
By default, when you create an API key, the key is saved into the
7171
database.
7272

73+
You can either enter the key manually or use the Generate button next to
74+
the key field. The key field is masked by default, and the password
75+
widget provides a button to reveal the value when needed.
76+
7377
If you want to manage them via serve environment settings use
7478
auth_api_key_server_env.
7579

auth_api_key/models/auth_api_key.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Copyright 2018 ACSONE SA/NV
22
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
33

4+
import secrets
5+
46
from odoo import api, fields, models, tools
57
from odoo.exceptions import AccessError, ValidationError
68
from odoo.tools import consteq
@@ -12,7 +14,6 @@ class AuthApiKey(models.Model):
1214

1315
name = fields.Char(required=True)
1416
key = fields.Char(
15-
required=True,
1617
help="""The API key. Enter a dummy value in this field if it is
1718
obtained from the server environment configuration.""",
1819
)
@@ -31,6 +32,28 @@ class AuthApiKey(models.Model):
3132

3233
_name_uniq = models.Constraint("unique(name)", "Api Key name must be unique.")
3334

35+
@api.model
36+
def _generate_random_key_value(self):
37+
"""Return a random API key value.
38+
39+
The token is generated by the Odoo server instance so XML data and the
40+
UI button can create a secret without storing any default value in the
41+
module sources.
42+
"""
43+
return secrets.token_urlsafe(32)
44+
45+
def generate_random_key(self, api_key_ids=None):
46+
"""Generate a key for records that do not have one yet.
47+
48+
:param list api_key_ids: optional record IDs, mainly used by XML data
49+
function calls where the method is invoked on the model.
50+
:return: True when the operation completed.
51+
"""
52+
api_keys = self.browse(api_key_ids) if api_key_ids else self
53+
for api_key in api_keys.filtered(lambda record: not record.key):
54+
api_key.key = api_key._generate_random_key_value()
55+
return True
56+
3457
@api.model
3558
def _retrieve_api_key(self, key):
3659
return self.browse(self._retrieve_api_key_id(key))

auth_api_key/readme/CONFIGURE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,9 @@ The api key menu is available into Settings \> Technical in debug mode.
22
By default, when you create an API key, the key is saved into the
33
database.
44

5+
You can either enter the key manually or use the Generate button next to
6+
the key field. The key field is masked by default, and the password widget
7+
provides a button to reveal the value when needed.
8+
59
If you want to manage them via serve environment settings use
610
auth_api_key_server_env.

auth_api_key/static/description/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,9 @@ <h2><a class="toc-backref" href="#toc-entry-1">Configuration</a></h2>
416416
<p>The api key menu is available into Settings &gt; Technical in debug mode.
417417
By default, when you create an API key, the key is saved into the
418418
database.</p>
419+
<p>You can either enter the key manually or use the Generate button next to
420+
the key field. The key field is masked by default, and the password
421+
widget provides a button to reveal the value when needed.</p>
419422
<p>If you want to manage them via serve environment settings use
420423
auth_api_key_server_env.</p>
421424
</div>

auth_api_key/tests/test_auth_api_key.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,34 @@ def setUpClass(cls, *args, **kwargs):
2525
{"name": "good", "user_id": cls.demo_user.id, "key": "api_key"}
2626
)
2727

28+
def test_00_generate_random_key_sets_empty_key(self):
29+
"""Test random key generation fills an empty API key."""
30+
api_key = self.AuthApiKey.create(
31+
{"name": "generated", "user_id": self.demo_user.id}
32+
)
33+
api_key.generate_random_key()
34+
self.assertTrue(api_key.key)
35+
self.assertEqual(
36+
self.AuthApiKey._retrieve_uid_from_api_key(api_key.key),
37+
self.demo_user.id,
38+
)
39+
40+
def test_01_generate_random_key_keeps_existing_key(self):
41+
"""Test random key generation does not overwrite an existing key."""
42+
api_key = self.AuthApiKey.create(
43+
{"name": "existing", "user_id": self.demo_user.id, "key": "existing_key"}
44+
)
45+
api_key.generate_random_key()
46+
self.assertEqual(api_key.key, "existing_key")
47+
48+
def test_02_generate_random_key_accepts_xml_style_ids(self):
49+
"""Test random key generation accepts the XML function-call argument."""
50+
api_key = self.AuthApiKey.create(
51+
{"name": "xml-style", "user_id": self.demo_user.id}
52+
)
53+
self.AuthApiKey.generate_random_key([api_key.id])
54+
self.assertTrue(api_key.key)
55+
2856
def test_lookup_key_from_db(self):
2957
demo_user = self.demo_user
3058
self.assertEqual(

auth_api_key/views/auth_api_key.xml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,18 @@
2121
</h1>
2222
<group name="config" colspan="4" col="4">
2323
<field name="user_id" colspan="4" />
24-
<field name="key" colspan="4" password="True" />
24+
<label for="key" />
25+
<div class="o_row" colspan="3">
26+
<!-- The password widget masks the key and provides a reveal button. -->
27+
<field name="key" widget="password" />
28+
<button
29+
name="generate_random_key"
30+
string="Generate"
31+
type="object"
32+
class="btn-secondary"
33+
invisible="key"
34+
/>
35+
</div>
2536
</group>
2637
</sheet>
2738
</form>

0 commit comments

Comments
 (0)