Completed
Pull Request — master (#2917)
by Lakshmi
05:16
created

KeyValuePairController._get_new_scope()   A

Complexity

Conditions 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
dl 0
loc 8
rs 9.4285
c 1
b 0
f 0
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import pecan
17
from pecan import abort
18
import six
19
from mongoengine import ValidationError
20
21
from st2api.controllers.resource import ResourceController
22
from st2common import log as logging
23
from st2common.constants.keyvalue import DEPRECATED_SYSTEM_SCOPE, DEPRECATED_USER_SCOPE
24
from st2common.constants.keyvalue import SYSTEM_SCOPE, USER_SCOPE, ALLOWED_SCOPES
25
from st2common.exceptions.db import StackStormDBObjectNotFoundError
26
from st2common.exceptions.keyvalue import CryptoKeyNotSetupException, InvalidScopeException
27
from st2common.models.api.keyvalue import KeyValuePairAPI
28
from st2common.models.api.keyvalue import KeyValuePairSetAPI
29
from st2common.models.api.base import jsexpose
30
from st2common.persistence.keyvalue import KeyValuePair
31
from st2common.services import coordination
32
from st2common.services.keyvalues import get_key_reference
33
from st2common.util.api import get_requester
34
from st2common.exceptions.rbac import AccessDeniedError
35
from st2common.rbac.utils import request_user_is_admin
36
from st2common.rbac.utils import assert_request_user_is_admin_if_user_query_param_is_provider
37
38
http_client = six.moves.http_client
39
40
LOG = logging.getLogger(__name__)
41
42
__all__ = [
43
    'KeyValuePairController'
44
]
45
46
47
class KeyValuePairController(ResourceController):
48
    """
49
    Implements the REST endpoint for managing the key value store.
50
    """
51
52
    model = KeyValuePairAPI
53
    access = KeyValuePair
54
    supported_filters = {
55
        'prefix': 'name__startswith',
56
        'scope': 'scope'
57
    }
58
59
    def __init__(self):
60
        super(KeyValuePairController, self).__init__()
61
        self._coordinator = coordination.get_coordinator()
62
        self.get_one_db_method = self._get_by_name
63
64
    @jsexpose(arg_types=[str, str, str, bool])
65
    def get_one(self, name, scope=None, user=None, decrypt=False):
0 ignored issues
show
Bug introduced by
Arguments number differs from overridden 'get_one' method
Loading history...
66
        """
67
            List key by name.
68
69
            Handle:
70
                GET /keys/key1
71
        """
72
        if not scope:
73
            scope = SYSTEM_SCOPE
74
75
        scope = self._get_new_scope(scope)
76
        self._validate_scope(scope=scope)
77
78
        if user:
79
            # Providing a user implies a user scope
80
            scope = USER_SCOPE
81
82
        requester_user = get_requester()
83
        user = user or requester_user
84
        is_admin = request_user_is_admin(request=pecan.request)
85
86
        # User needs to be either admin or requesting item for itself
87
        self._validate_decrypt_query_parameter(decrypt=decrypt, scope=scope, is_admin=is_admin)
88
89
        # Validate that the authenticated user is admin if user query param is provided
90
        assert_request_user_is_admin_if_user_query_param_is_provider(request=pecan.request,
91
                                                                     user=user)
92
93
        key_ref = get_key_reference(scope=scope, name=name, user=user)
94
        from_model_kwargs = {'mask_secrets': not decrypt}
95
        kvp_api = self._get_one_by_scope_and_name(
96
            name=key_ref,
97
            scope=scope,
98
            from_model_kwargs=from_model_kwargs
99
        )
100
101
        return kvp_api
102
103
    @jsexpose(arg_types=[str, str, str, bool])
104
    def get_all(self, prefix=None, scope=None, user=None, decrypt=False, **kwargs):
105
        """
106
            List all keys.
107
108
            Handles requests:
109
                GET /keys/
110
        """
111
        if not scope:
112
            scope = SYSTEM_SCOPE
113
114
        if user:
115
            # Providing a user implies a user scope
116
            scope = USER_SCOPE
117
118
        scope = self._get_new_scope(scope)
119
        requester_user = get_requester()
120
        user = user or requester_user
121
        is_all_scope = (scope == 'all')
122
        is_admin = request_user_is_admin(request=pecan.request)
123
124
        if is_all_scope and not is_admin:
125
            msg = '"all" scope requires administrator access'
126
            raise AccessDeniedError(message=msg, user_db=requester_user)
127
128
        # User needs to be either admin or requesting items for themselves
129
        self._validate_decrypt_query_parameter(decrypt=decrypt, scope=scope, is_admin=is_admin)
130
131
        # Validate that the authenticated user is admin if user query param is provided
132
        assert_request_user_is_admin_if_user_query_param_is_provider(request=pecan.request,
133
                                                                     user=user)
134
135
        from_model_kwargs = {'mask_secrets': not decrypt}
136
        kwargs['prefix'] = prefix
137
138
        if scope and scope not in ['all']:
139
            self._validate_scope(scope=scope)
140
            kwargs['scope'] = scope
141
142
        if scope == USER_SCOPE:
143
            # Make sure we only returned values scoped to current user
144
            if kwargs['prefix']:
145
                kwargs['prefix'] = get_key_reference(name=kwargs['prefix'], scope=scope,
146
                                                     user=requester_user)
147
            else:
148
                kwargs['prefix'] = get_key_reference(name='', scope=scope,
149
                                                     user=user)
150
151
        kvp_apis = super(KeyValuePairController, self)._get_all(from_model_kwargs=from_model_kwargs,
152
                                                                **kwargs)
153
        return kvp_apis
154
155
    @jsexpose(arg_types=[str, str, str], body_cls=KeyValuePairSetAPI)
156
    def put(self, kvp, name, scope=None):
157
        """
158
        Create a new entry or update an existing one.
159
        """
160
        if not scope:
161
            scope = SYSTEM_SCOPE
162
        requester_user = get_requester()
163
164
        scope = getattr(kvp, 'scope', scope)
165
        scope = self._get_new_scope(scope)
166
        self._validate_scope(scope=scope)
167
168
        user = getattr(kvp, 'user', requester_user) or requester_user
169
170
        # Validate that the authenticated user is admin if user query param is provided
171
        assert_request_user_is_admin_if_user_query_param_is_provider(request=pecan.request,
172
                                                                     user=user)
173
174
        key_ref = get_key_reference(scope=scope, name=name, user=user)
175
        lock_name = self._get_lock_name_for_key(name=key_ref, scope=scope)
176
        LOG.debug('PUT scope: %s, name: %s', scope, name)
177
        # TODO: Custom permission check since the key doesn't need to exist here
178
179
        # Note: We use lock to avoid a race
180
        with self._coordinator.get_lock(lock_name):
181
            try:
182
                existing_kvp_api = self._get_one_by_scope_and_name(
183
                    scope=scope,
184
                    name=key_ref
185
                )
186
            except StackStormDBObjectNotFoundError:
187
                existing_kvp_api = None
188
189
            kvp.name = key_ref
190
            kvp.scope = scope
191
192
            try:
193
                kvp_db = KeyValuePairAPI.to_model(kvp)
194
195
                if existing_kvp_api:
196
                    kvp_db.id = existing_kvp_api.id
197
198
                kvp_db = KeyValuePair.add_or_update(kvp_db)
199
            except (ValidationError, ValueError) as e:
200
                LOG.exception('Validation failed for key value data=%s', kvp)
201
                abort(http_client.BAD_REQUEST, str(e))
202
                return
203
            except CryptoKeyNotSetupException as e:
204
                LOG.exception(str(e))
205
                abort(http_client.BAD_REQUEST, str(e))
206
                return
207
            except InvalidScopeException as e:
208
                LOG.exception(str(e))
209
                abort(http_client.BAD_REQUEST, str(e))
210
                return
211
        extra = {'kvp_db': kvp_db}
212
        LOG.audit('KeyValuePair updated. KeyValuePair.id=%s' % (kvp_db.id), extra=extra)
213
214
        kvp_api = KeyValuePairAPI.from_model(kvp_db)
215
        return kvp_api
216
217
    @jsexpose(arg_types=[str, str, str], status_code=http_client.NO_CONTENT)
218
    def delete(self, name, scope=None, user=None):
219
        """
220
            Delete the key value pair.
221
222
            Handles requests:
223
                DELETE /keys/1
224
        """
225
        if not scope:
226
            scope = SYSTEM_SCOPE
227
228
        scope = self._get_new_scope(scope)
229
        self._validate_scope(scope=scope)
230
231
        requester_user = get_requester()
232
        user = user or requester_user
233
234
        # Validate that the authenticated user is admin if user query param is provided
235
        assert_request_user_is_admin_if_user_query_param_is_provider(request=pecan.request,
236
                                                                     user=user)
237
238
        key_ref = get_key_reference(scope=scope, name=name, user=user)
239
        lock_name = self._get_lock_name_for_key(name=key_ref, scope=scope)
240
241
        # Note: We use lock to avoid a race
242
        with self._coordinator.get_lock(lock_name):
243
            from_model_kwargs = {'mask_secrets': True}
244
            kvp_api = self._get_one_by_scope_and_name(
245
                name=key_ref,
246
                scope=scope,
247
                from_model_kwargs=from_model_kwargs
248
            )
249
250
            kvp_db = KeyValuePairAPI.to_model(kvp_api)
251
252
            LOG.debug('DELETE /keys/ lookup with scope=%s name=%s found object: %s',
253
                      scope, name, kvp_db)
254
255
            try:
256
                KeyValuePair.delete(kvp_db)
257
            except Exception as e:
258
                LOG.exception('Database delete encountered exception during '
259
                              'delete of name="%s". ', name)
260
                abort(http_client.INTERNAL_SERVER_ERROR, str(e))
261
                return
262
263
        extra = {'kvp_db': kvp_db}
264
        LOG.audit('KeyValuePair deleted. KeyValuePair.id=%s' % (kvp_db.id), extra=extra)
265
266
    def _get_lock_name_for_key(self, name, scope=SYSTEM_SCOPE):
267
        """
268
        Retrieve a coordination lock name for the provided datastore item name.
269
270
        :param name: Datastore item name (PK).
271
        :type name: ``str``
272
        """
273
        lock_name = 'kvp-crud-%s.%s' % (scope, name)
274
        return lock_name
275
276
    def _validate_decrypt_query_parameter(self, decrypt, scope, is_admin):
277
        """
278
        Validate that the provider user is either admin or requesting to decrypt value for
279
        themselves.
280
        """
281
        requester_user = get_requester()
282
283
        if decrypt and (scope != USER_SCOPE and not is_admin):
284
            msg = 'Decrypt option requires administrator access'
285
            raise AccessDeniedError(message=msg, user_db=requester_user)
286
287
    def _validate_scope(self, scope):
288
        if scope not in ALLOWED_SCOPES:
289
            msg = 'Scope %s is not in allowed scopes list: %s.' % (scope, ALLOWED_SCOPES)
290
            raise ValueError(msg)
291
292
    def _get_new_scope(self, scope):
293
        if scope == DEPRECATED_SYSTEM_SCOPE:
294
            return SYSTEM_SCOPE
295
296
        if scope == DEPRECATED_USER_SCOPE:
297
            return USER_SCOPE
298
299
        return scope
300