Browse Source

Implement scope_type checking for credentials

This change adds tests cases for the default roles keystone
supports at install time. It also modifies the policies for the
credentials API to be more self-service by properly checking
for various scopes.

Closes-Bug: 1788415
Partial-Bug: 968696

Change-Id: Ifedb7798c96930b6cc0f91159a14a21ac4b02f9f
tags/15.0.0.0rc1
Lance Bragstad 9 months ago
parent
commit
239bed09a9

+ 53
- 8
keystone/api/credentials.py View File

@@ -21,16 +21,31 @@ from six.moves import http_client
21 21
 from keystone.common import provider_api
22 22
 from keystone.common import rbac_enforcer
23 23
 from keystone.common import validation
24
+import keystone.conf
24 25
 from keystone.credential import schema
25 26
 from keystone import exception
26 27
 from keystone.i18n import _
27 28
 from keystone.server import flask as ks_flask
28 29
 
29
-
30
+CONF = keystone.conf.CONF
30 31
 PROVIDERS = provider_api.ProviderAPIs
31 32
 ENFORCER = rbac_enforcer.RBACEnforcer
32 33
 
33 34
 
35
+def _build_target_enforcement():
36
+    target = {}
37
+    try:
38
+        target['credential'] = PROVIDERS.credential_api.get_credential(
39
+            flask.request.view_args.get('credential_id')
40
+        )
41
+    except exception.NotFound:  # nosec
42
+        # Defer existance in the event the credential doesn't exist, we'll
43
+        # check this later anyway.
44
+        pass
45
+
46
+    return target
47
+
48
+
34 49
 class CredentialResource(ks_flask.ResourceBase):
35 50
     collection_key = 'credentials'
36 51
     member_key = 'credential'
@@ -75,17 +90,34 @@ class CredentialResource(ks_flask.ResourceBase):
75 90
 
76 91
     def _list_credentials(self):
77 92
         filters = ['user_id', 'type']
93
+        if not self.oslo_context.system_scope:
94
+            target = {'credential': {'user_id': self.oslo_context.user_id}}
95
+        else:
96
+            target = None
78 97
         ENFORCER.enforce_call(action='identity:list_credentials',
79
-                              filters=filters)
98
+                              filters=filters, target_attr=target)
80 99
         hints = self.build_driver_hints(filters)
81 100
         refs = PROVIDERS.credential_api.list_credentials(hints)
101
+        # If the request was filtered, make sure to return only the
102
+        # credentials specific to that user. This makes it so that users with
103
+        # roles on projects can't see credentials that aren't theirs.
104
+        if (not self.oslo_context.system_scope and
105
+                CONF.oslo_policy.enforce_scope):
106
+            filtered_refs = []
107
+            for ref in refs:
108
+                if ref['user_id'] == target['credential']['user_id']:
109
+                    filtered_refs.append(ref)
110
+            refs = filtered_refs
82 111
         refs = [self._blob_to_json(r) for r in refs]
83 112
         return self.wrap_collection(refs, hints=hints)
84 113
 
85 114
     def _get_credential(self, credential_id):
86
-        ENFORCER.enforce_call(action='identity:get_credential')
87
-        ref = PROVIDERS.credential_api.get_credential(credential_id)
88
-        return self.wrap_member(self._blob_to_json(ref))
115
+        ENFORCER.enforce_call(
116
+            action='identity:get_credential',
117
+            build_target=_build_target_enforcement
118
+        )
119
+        credential = PROVIDERS.credential_api.get_credential(credential_id)
120
+        return self.wrap_member(self._blob_to_json(credential))
89 121
 
90 122
     def get(self, credential_id=None):
91 123
         # Get Credential or List of credentials.
@@ -97,8 +129,12 @@ class CredentialResource(ks_flask.ResourceBase):
97 129
 
98 130
     def post(self):
99 131
         # Create a new credential
100
-        ENFORCER.enforce_call(action='identity:create_credential')
101 132
         credential = flask.request.json.get('credential', {})
133
+        target = {}
134
+        target['credential'] = credential
135
+        ENFORCER.enforce_call(
136
+            action='identity:create_credential', target_attr=target
137
+        )
102 138
         validation.lazy_validate(schema.credential_create, credential)
103 139
         trust_id = getattr(self.oslo_context, 'trust_id', None)
104 140
         ref = self._assign_unique_id(
@@ -108,7 +144,12 @@ class CredentialResource(ks_flask.ResourceBase):
108 144
 
109 145
     def patch(self, credential_id):
110 146
         # Update Credential
111
-        ENFORCER.enforce_call(action='identity:update_credential')
147
+        ENFORCER.enforce_call(
148
+            action='identity:update_credential',
149
+            build_target=_build_target_enforcement
150
+        )
151
+        PROVIDERS.credential_api.get_credential(credential_id)
152
+
112 153
         credential = flask.request.json.get('credential', {})
113 154
         validation.lazy_validate(schema.credential_update, credential)
114 155
         self._require_matching_id(credential)
@@ -118,7 +159,11 @@ class CredentialResource(ks_flask.ResourceBase):
118 159
 
119 160
     def delete(self, credential_id):
120 161
         # Delete credentials
121
-        ENFORCER.enforce_call(action='identity:delete_credential')
162
+        ENFORCER.enforce_call(
163
+            action='identity:delete_credential',
164
+            build_target=_build_target_enforcement
165
+        )
166
+
122 167
         return (PROVIDERS.credential_api.delete_credential(credential_id),
123 168
                 http_client.NO_CONTENT)
124 169
 

+ 78
- 25
keystone/common/policies/credential.py View File

@@ -10,56 +10,109 @@
10 10
 # License for the specific language governing permissions and limitations
11 11
 # under the License.
12 12
 
13
+from oslo_log import versionutils
13 14
 from oslo_policy import policy
14 15
 
15 16
 from keystone.common.policies import base
16 17
 
18
+SYSTEM_READER_OR_CRED_OWNER = (
19
+    '(role:reader and system_scope:all) '
20
+    'or user_id:%(target.credential.user_id)s'
21
+)
22
+SYSTEM_MEMBER_OR_CRED_OWNER = (
23
+    '(role:member and system_scope:all) '
24
+    'or user_id:%(target.credential.user_id)s'
25
+)
26
+SYSTEM_ADMIN_OR_CRED_OWNER = (
27
+    '(role:admin and system_scope:all) '
28
+    'or user_id:%(target.credential.user_id)s'
29
+)
30
+
31
+DEPRECATED_REASON = (
32
+    'As of the Stein release, the credential API now understands how to '
33
+    'handle system-scoped tokens in addition to project-scoped tokens, making '
34
+    'the API more accessible to users without compromising security or '
35
+    'manageability for administrators. The new default policies for this API '
36
+    'account for these changes automatically.'
37
+)
38
+deprecated_get_credential = policy.DeprecatedRule(
39
+    name=base.IDENTITY % 'get_credential',
40
+    check_str=base.RULE_ADMIN_REQUIRED
41
+)
42
+deprecated_list_credentials = policy.DeprecatedRule(
43
+    name=base.IDENTITY % 'list_credentials',
44
+    check_str=base.RULE_ADMIN_REQUIRED
45
+)
46
+deprecated_create_credential = policy.DeprecatedRule(
47
+    name=base.IDENTITY % 'create_credential',
48
+    check_str=base.RULE_ADMIN_REQUIRED
49
+)
50
+deprecated_update_credential = policy.DeprecatedRule(
51
+    name=base.IDENTITY % 'update_credential',
52
+    check_str=base.RULE_ADMIN_REQUIRED
53
+)
54
+deprecated_delete_credential = policy.DeprecatedRule(
55
+    name=base.IDENTITY % 'delete_credential',
56
+    check_str=base.RULE_ADMIN_REQUIRED
57
+)
58
+
59
+
17 60
 credential_policies = [
18 61
     policy.DocumentedRuleDefault(
19 62
         name=base.IDENTITY % 'get_credential',
20
-        check_str=base.RULE_ADMIN_REQUIRED,
21
-        # FIXME(lbragstad): Credentials aren't really project-scoped or
22
-        # system-scoped. Instead, they are tied to a user. If this API is
23
-        # called with a system-scoped token, it's a system-administrator and
24
-        # they should be able to get any credential for management reasons. If
25
-        # this API is called with a project-scoped token, then extra
26
-        # enforcement needs to happen based on who created the credential, what
27
-        # projects they are members of, and the project the token is scoped to.
28
-        # When we fully support the second case, we can add `project` to the
29
-        # list of scope_types. This comment applies to the rest of the policies
30
-        # in this module.
31
-        # scope_types=['system', 'project'],
63
+        check_str=SYSTEM_READER_OR_CRED_OWNER,
64
+        scope_types=['system', 'project'],
32 65
         description='Show credentials details.',
33 66
         operations=[{'path': '/v3/credentials/{credential_id}',
34
-                     'method': 'GET'}]),
67
+                     'method': 'GET'}],
68
+        deprecated_rule=deprecated_get_credential,
69
+        deprecated_reason=DEPRECATED_REASON,
70
+        deprecated_since=versionutils.deprecated.STEIN
71
+    ),
35 72
     policy.DocumentedRuleDefault(
36 73
         name=base.IDENTITY % 'list_credentials',
37
-        check_str=base.RULE_ADMIN_REQUIRED,
38
-        # scope_types=['system', 'project'],
74
+        check_str=SYSTEM_READER_OR_CRED_OWNER,
75
+        scope_types=['system', 'project'],
39 76
         description='List credentials.',
40 77
         operations=[{'path': '/v3/credentials',
41
-                     'method': 'GET'}]),
78
+                     'method': 'GET'}],
79
+        deprecated_rule=deprecated_list_credentials,
80
+        deprecated_reason=DEPRECATED_REASON,
81
+        deprecated_since=versionutils.deprecated.STEIN
82
+    ),
42 83
     policy.DocumentedRuleDefault(
43 84
         name=base.IDENTITY % 'create_credential',
44
-        check_str=base.RULE_ADMIN_REQUIRED,
45
-        # scope_types=['system', 'project'],
85
+        check_str=SYSTEM_ADMIN_OR_CRED_OWNER,
86
+        scope_types=['system', 'project'],
46 87
         description='Create credential.',
47 88
         operations=[{'path': '/v3/credentials',
48
-                     'method': 'POST'}]),
89
+                     'method': 'POST'}],
90
+        deprecated_rule=deprecated_create_credential,
91
+        deprecated_reason=DEPRECATED_REASON,
92
+        deprecated_since=versionutils.deprecated.STEIN
93
+    ),
49 94
     policy.DocumentedRuleDefault(
50 95
         name=base.IDENTITY % 'update_credential',
51
-        check_str=base.RULE_ADMIN_REQUIRED,
52
-        # scope_types=['system', 'project'],
96
+        check_str=SYSTEM_MEMBER_OR_CRED_OWNER,
97
+        scope_types=['system', 'project'],
53 98
         description='Update credential.',
54 99
         operations=[{'path': '/v3/credentials/{credential_id}',
55
-                     'method': 'PATCH'}]),
100
+                     'method': 'PATCH'}],
101
+        deprecated_rule=deprecated_update_credential,
102
+        deprecated_reason=DEPRECATED_REASON,
103
+        deprecated_since=versionutils.deprecated.STEIN
104
+    ),
56 105
     policy.DocumentedRuleDefault(
57 106
         name=base.IDENTITY % 'delete_credential',
58
-        check_str=base.RULE_ADMIN_REQUIRED,
59
-        # scope_types=['system', 'project'],
107
+        check_str=SYSTEM_ADMIN_OR_CRED_OWNER,
108
+        scope_types=['system', 'project'],
60 109
         description='Delete credential.',
61 110
         operations=[{'path': '/v3/credentials/{credential_id}',
62
-                     'method': 'DELETE'}])
111
+                     'method': 'DELETE'}],
112
+        deprecated_rule=deprecated_delete_credential,
113
+        deprecated_reason=DEPRECATED_REASON,
114
+        deprecated_since=versionutils.deprecated.STEIN
115
+    )
63 116
 ]
64 117
 
65 118
 

+ 1
- 0
keystone/tests/unit/base_classes.py View File

@@ -41,6 +41,7 @@ class TestCaseWithBootstrap(core.BaseTestCase):
41 41
         self.useFixture(database.Database())
42 42
         super(TestCaseWithBootstrap, self).setUp()
43 43
         self.config_fixture = self.useFixture(config_fixture.Config(CONF))
44
+        CONF(args=[], project='keystone')
44 45
         self.useFixture(
45 46
             ksfixtures.KeyRepository(
46 47
                 self.config_fixture,

+ 0
- 0
keystone/tests/unit/protection/__init__.py View File


+ 0
- 0
keystone/tests/unit/protection/v3/__init__.py View File


+ 1137
- 0
keystone/tests/unit/protection/v3/test_credentials.py
File diff suppressed because it is too large
View File


+ 15
- 3
keystone/tests/unit/test_v3_credential.py View File

@@ -113,6 +113,11 @@ class CredentialTestCase(CredentialBaseTestCase):
113 113
 
114 114
     def test_list_credentials_filtered_by_type(self):
115 115
         """Call ``GET  /credentials?type={type}``."""
116
+        PROVIDERS.assignment_api.create_system_grant_for_user(
117
+            self.user_id, self.role_id
118
+        )
119
+        token = self.get_system_scoped_token()
120
+
116 121
         # The type ec2 was chosen, instead of a random string,
117 122
         # because the type must be in the list of supported types
118 123
         ec2_credential = unit.new_credential_ref(user_id=uuid.uuid4().hex,
@@ -123,14 +128,14 @@ class CredentialTestCase(CredentialBaseTestCase):
123 128
             ec2_credential['id'], ec2_credential)
124 129
 
125 130
         # The type cert was chosen for the same reason as ec2
126
-        r = self.get('/credentials?type=cert')
131
+        r = self.get('/credentials?type=cert', token=token)
127 132
 
128 133
         # Testing the filter for two different types
129 134
         self.assertValidCredentialListResponse(r, ref=self.credential)
130 135
         for cred in r.result['credentials']:
131 136
             self.assertEqual('cert', cred['type'])
132 137
 
133
-        r_ec2 = self.get('/credentials?type=ec2')
138
+        r_ec2 = self.get('/credentials?type=ec2', token=token)
134 139
         self.assertThat(r_ec2.result['credentials'], matchers.HasLength(1))
135 140
         cred_ec2 = r_ec2.result['credentials'][0]
136 141
 
@@ -143,6 +148,11 @@ class CredentialTestCase(CredentialBaseTestCase):
143 148
         user1_id = uuid.uuid4().hex
144 149
         user2_id = uuid.uuid4().hex
145 150
 
151
+        PROVIDERS.assignment_api.create_system_grant_for_user(
152
+            self.user_id, self.role_id
153
+        )
154
+        token = self.get_system_scoped_token()
155
+
146 156
         # Creating credentials for two different users
147 157
         credential_user1_ec2 = unit.new_credential_ref(user_id=user1_id,
148 158
                                                        type=CRED_TYPE_EC2)
@@ -156,7 +166,9 @@ class CredentialTestCase(CredentialBaseTestCase):
156 166
         PROVIDERS.credential_api.create_credential(
157 167
             credential_user2_cert['id'], credential_user2_cert)
158 168
 
159
-        r = self.get('/credentials?user_id=%s&type=ec2' % user1_id)
169
+        r = self.get(
170
+            '/credentials?user_id=%s&type=ec2' % user1_id, token=token
171
+        )
160 172
         self.assertValidCredentialListResponse(r, ref=credential_user1_ec2)
161 173
         self.assertThat(r.result['credentials'], matchers.HasLength(1))
162 174
         cred = r.result['credentials'][0]

+ 25
- 0
releasenotes/notes/bug-1788415-3190279e9c900f76.yaml View File

@@ -0,0 +1,25 @@
1
+---
2
+upgrade:
3
+  - |
4
+    [`bug 1788415 <https://bugs.launchpad.net/keystone/+bug/1788415>`_]
5
+    [`bug 968696 <https://bugs.launchpad.net/keystone/+bug/968696>`_]
6
+    Policies protecting the ``/v3/credentials`` API have changed defaults in
7
+    order to make the credentials API more accessible for all users and not
8
+    just operators or system administrator. Please consider these updates when
9
+    using this version of keystone since it could affect API behavior in your
10
+    deployment, especially if you're using a customized policy file.
11
+security:
12
+  - |
13
+    [`bug 1788415 <https://bugs.launchpad.net/keystone/+bug/1788415>`_]
14
+    [`bug 968696 <https://bugs.launchpad.net/keystone/+bug/968696>`_]
15
+    More granular policy checks have been applied to the credential API in
16
+    order to make it more self-service for users. By default, end users will
17
+    now have the ability to manage their credentials.
18
+fixes:
19
+  - |
20
+    [`bug 1788415 <https://bugs.launchpad.net/keystone/+bug/1788415>`_]
21
+    [`bug 968696 <https://bugs.launchpad.net/keystone/+bug/968696>`_]
22
+    Improved self-service support has been implemented in the credential API.
23
+    This means that end users have the ability to manage their own credentials
24
+    as opposed to filing tickets to have deployment administrators manage
25
+    credentials for users.

Loading…
Cancel
Save