Repository: netbox-community/netbox

Issue:
Secret role restrictions not enforced via API
### Environment
* Python version:  3.6.8
* NetBox version: 2.7.4

(seen also in this comment https://github.com/netbox-community/netbox/issues/2869#issuecomment-584106870 on closed ticket )

### Steps to Reproduce
1. Create a non admin account with granted all secret* permission (in my case it's thru and AD account and group)
2. Add public key to account
3. Activate public key with an admin acount
4. User try to see a secret (unlock button is stil grey even after log off log on)
5. Go on one secret page (/secrets/secrets/$secret-id/)
6. we've got warning message : "you do not have permission to view secret"
7. clic on edit button 
8. **unlock button is now green and works**

### Expected Behavior
step 4 : unlock button is green and permit to add private key to session then see password
step 6 : unlock button is green and permit to add private key to session then see password

### Observed Behavior
step 4 : unlock button is stil grey and permission deny message when try to use it
step 6 : we've got warning message : "you do not have permission to view secret"

The fact that we can see secret in step 8 is also an issue as the permission should works exactly the same way in any view.

Best regards,
Al


Discussion:
The root issue here is that secret role assignment is not being enforced on API calls. When you create a secret, it must be assigned to a role. This role is assigned certain users and/or groups which have permission to decrypt the secrets belonging to it. Users which don't belong to the role should not be able to decrypt the secret.

Everything above is correct until step eight: The user should not be given the option to decrypt the secret, assuming he has not been assigned to the secret's role. Further, decryption via the API should fail, and it currently does not.

We'll need to take measures to ensure that

1) The UI displays the "unlock" button only if the user belongs to the secret's role, and
2) Secrets retrieved via the API are decrypted _only_ if the user belongs to the secret's role.
This answer is awesome.

It's permit me to find how to manage access to some passwords and not the other one. I was searching for it and just found how to : edit secret role, i did not find it previously.

Thanks!

Resolving patch:
diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py
--- a/netbox/secrets/api/views.py
+++ b/netbox/secrets/api/views.py
@@ -93,8 +93,8 @@ def retrieve(self, request, *args, **kwargs):
 
         secret = self.get_object()
 
-        # Attempt to decrypt the secret if the master key is known
-        if self.master_key is not None:
+        # Attempt to decrypt the secret if the user is permitted and the master key is known
+        if secret.decryptable_by(request.user) and self.master_key is not None:
             secret.decrypt(self.master_key)
 
         serializer = self.get_serializer(secret)
@@ -111,7 +111,9 @@ def list(self, request, *args, **kwargs):
             if self.master_key is not None:
                 secrets = []
                 for secret in page:
-                    secret.decrypt(self.master_key)
+                    # Enforce role permissions
+                    if secret.decryptable_by(request.user):
+                        secret.decrypt(self.master_key)
                     secrets.append(secret)
                 serializer = self.get_serializer(secrets, many=True)
             else:


Test patch:
diff --git a/netbox/secrets/tests/test_api.py b/netbox/secrets/tests/test_api.py
--- a/netbox/secrets/tests/test_api.py
+++ b/netbox/secrets/tests/test_api.py
@@ -5,7 +5,8 @@
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from secrets.models import Secret, SecretRole, SessionKey, UserKey
-from utilities.testing import APITestCase
+from users.models import Token
+from utilities.testing import APITestCase, create_test_user
 from .constants import PRIVATE_KEY, PUBLIC_KEY
 
 
@@ -131,7 +132,15 @@ class SecretTest(APITestCase):
 
     def setUp(self):
 
-        super().setUp()
+        # Create a non-superuser test user
+        self.user = create_test_user('testuser', permissions=(
+            'secrets.add_secret',
+            'secrets.change_secret',
+            'secrets.delete_secret',
+            'secrets.view_secret',
+        ))
+        self.token = Token.objects.create(user=self.user)
+        self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)}
 
         userkey = UserKey(user=self.user, public_key=PUBLIC_KEY)
         userkey.save()
@@ -144,11 +153,11 @@ def setUp(self):
             'HTTP_X_SESSION_KEY': base64.b64encode(session_key.key),
         }
 
-        self.plaintext = {
-            'secret1': 'Secret #1 Plaintext',
-            'secret2': 'Secret #2 Plaintext',
-            'secret3': 'Secret #3 Plaintext',
-        }
+        self.plaintexts = (
+            'Secret #1 Plaintext',
+            'Secret #2 Plaintext',
+            'Secret #3 Plaintext',
+        )
 
         site = Site.objects.create(name='Test Site 1', slug='test-site-1')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
@@ -160,17 +169,17 @@ def setUp(self):
         self.secretrole1 = SecretRole.objects.create(name='Test Secret Role 1', slug='test-secret-role-1')
         self.secretrole2 = SecretRole.objects.create(name='Test Secret Role 2', slug='test-secret-role-2')
         self.secret1 = Secret(
-            device=self.device, role=self.secretrole1, name='Test Secret 1', plaintext=self.plaintext['secret1']
+            device=self.device, role=self.secretrole1, name='Test Secret 1', plaintext=self.plaintexts[0]
         )
         self.secret1.encrypt(self.master_key)
         self.secret1.save()
         self.secret2 = Secret(
-            device=self.device, role=self.secretrole1, name='Test Secret 2', plaintext=self.plaintext['secret2']
+            device=self.device, role=self.secretrole1, name='Test Secret 2', plaintext=self.plaintexts[1]
         )
         self.secret2.encrypt(self.master_key)
         self.secret2.save()
         self.secret3 = Secret(
-            device=self.device, role=self.secretrole1, name='Test Secret 3', plaintext=self.plaintext['secret3']
+            device=self.device, role=self.secretrole1, name='Test Secret 3', plaintext=self.plaintexts[2]
         )
         self.secret3.encrypt(self.master_key)
         self.secret3.save()
@@ -178,16 +187,32 @@ def setUp(self):
     def test_get_secret(self):
 
         url = reverse('secrets-api:secret-detail', kwargs={'pk': self.secret1.pk})
+
+        # Secret plaintext not be decrypted as the user has not been assigned to the role
         response = self.client.get(url, **self.header)
+        self.assertIsNone(response.data['plaintext'])
 
-        self.assertEqual(response.data['plaintext'], self.plaintext['secret1'])
+        # The plaintext should be present once the user has been assigned to the role
+        self.secretrole1.users.add(self.user)
+        response = self.client.get(url, **self.header)
+        self.assertEqual(response.data['plaintext'], self.plaintexts[0])
 
     def test_list_secrets(self):
 
         url = reverse('secrets-api:secret-list')
+
+        # Secret plaintext not be decrypted as the user has not been assigned to the role
         response = self.client.get(url, **self.header)
+        self.assertEqual(response.data['count'], 3)
+        for secret in response.data['results']:
+            self.assertIsNone(secret['plaintext'])
 
+        # The plaintext should be present once the user has been assigned to the role
+        self.secretrole1.users.add(self.user)
+        response = self.client.get(url, **self.header)
         self.assertEqual(response.data['count'], 3)
+        for i, secret in enumerate(response.data['results']):
+            self.assertEqual(secret['plaintext'], self.plaintexts[i])
 
     def test_create_secret(self):
 
diff --git a/netbox/utilities/testing/utils.py b/netbox/utilities/testing/utils.py
--- a/netbox/utilities/testing/utils.py
+++ b/netbox/utilities/testing/utils.py
@@ -21,11 +21,13 @@ def post_data(data):
     return ret
 
 
-def create_test_user(username='testuser', permissions=list()):
+def create_test_user(username='testuser', permissions=None):
     """
     Create a User with the given permissions.
     """
     user = User.objects.create_user(username=username)
+    if permissions is None:
+        permissions = ()
     for perm_name in permissions:
         app, codename = perm_name.split('.')
         perm = Permission.objects.get(content_type__app_label=app, codename=codename)