Skip to content

Commit a784f99

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

6 files changed

Lines changed: 85 additions & 2 deletions

File tree

auth_api_key/README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ 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+
When you create an API key, a random key is generated automatically. You
74+
can replace it manually, or use the Generate Random Token button if an
75+
existing record has no key. The key field is masked by default, and the
76+
password widget provides a button to reveal the value when needed.
77+
7378
If you want to manage them via serve environment settings use
7479
auth_api_key_server_env.
7580

auth_api_key/models/auth_api_key.py

Lines changed: 31 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,7 @@ class AuthApiKey(models.Model):
1214

1315
name = fields.Char(required=True)
1416
key = fields.Char(
15-
required=True,
17+
default=lambda self: self._generate_random_key_value(),
1618
help="""The API key. Enter a dummy value in this field if it is
1719
obtained from the server environment configuration.""",
1820
)
@@ -31,6 +33,34 @@ class AuthApiKey(models.Model):
3133

3234
_name_uniq = models.Constraint("unique(name)", "Api Key name must be unique.")
3335

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

auth_api_key/readme/CONFIGURE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@ 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+
When you create an API key, a random key is generated automatically. You
6+
can replace it manually, or use the Generate Random Token button if an
7+
existing record has no key. The key field is masked by default, and the
8+
password widget provides a button to reveal the value when needed.
9+
510
If you want to manage them via serve environment settings use
611
auth_api_key_server_env.

auth_api_key/static/description/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,10 @@ <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>When you create an API key, a random key is generated automatically. You
420+
can replace it manually, or use the Generate Random Token button if an
421+
existing record has no key. The key field is masked by default, and the
422+
password widget provides a button to reveal the value when needed.</p>
419423
<p>If you want to manage them via serve environment settings use
420424
auth_api_key_server_env.</p>
421425
</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_create_without_key_generates_random_key(self):
29+
"""Test API key creation generates an API key when no key is provided."""
30+
api_key = self.AuthApiKey.create(
31+
{"name": "generated", "user_id": self.demo_user.id}
32+
)
33+
self.assertTrue(api_key.key)
34+
self.assertEqual(
35+
self.AuthApiKey._retrieve_uid_from_api_key(api_key.key),
36+
self.demo_user.id,
37+
)
38+
39+
def test_generate_random_key_keeps_existing_key(self):
40+
"""Test random key generation does not overwrite an existing key."""
41+
api_key = self.AuthApiKey.create(
42+
{"name": "existing", "user_id": self.demo_user.id, "key": "existing_key"}
43+
)
44+
api_key.generate_random_key()
45+
self.assertEqual(api_key.key, "existing_key")
46+
47+
def test_empty_key_is_rejected(self):
48+
"""Test empty API key values are rejected through the ORM."""
49+
with self.assertRaises(ValidationError), self.env.cr.savepoint():
50+
self.AuthApiKey.create(
51+
{"name": "empty", "user_id": self.demo_user.id, "key": ""}
52+
)
53+
with self.assertRaises(ValidationError), self.env.cr.savepoint():
54+
self.api_key_good.key = False
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" required="1" />
28+
<button
29+
name="generate_random_key"
30+
string="Generate Random Token"
31+
type="object"
32+
class="btn-secondary"
33+
invisible="key"
34+
/>
35+
</div>
2536
</group>
2637
</sheet>
2738
</form>

0 commit comments

Comments
 (0)