Test Failed
Push — master ( f325af...21460f )
by Tomaz
02:06 queued 10s
created

st2api/st2api/controllers/v1/keyvalue.py (1 issue)

Labels
Severity
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
0 ignored issues
show
There seems to be a cyclic import (st2api.controllers.v1.pack_views -> st2api.controllers.v1.packs).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
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
from oslo_config import cfg
17
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 ALL_SCOPE, FULL_SYSTEM_SCOPE
24
from st2common.constants.keyvalue import FULL_USER_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.db.auth import UserDB
29
from st2common.persistence.keyvalue import KeyValuePair
30
from st2common.services import coordination
31
from st2common.services.keyvalues import get_key_reference
32
from st2common.util.keyvalue import get_datastore_full_scope
33
from st2common.exceptions.rbac import AccessDeniedError
34
from st2common.rbac import utils as rbac_utils
35
from st2common.router import abort
36
from st2common.router import Response
37
from st2common.rbac.utils import assert_user_is_admin_if_user_query_param_is_provided
38
39
http_client = six.moves.http_client
40
41
LOG = logging.getLogger(__name__)
42
43
__all__ = [
44
    'KeyValuePairController'
45
]
46
47
48
class KeyValuePairController(ResourceController):
49
    """
50
    Implements the REST endpoint for managing the key value store.
51
    """
52
53
    model = KeyValuePairAPI
54
    access = KeyValuePair
55
    supported_filters = {
56
        'prefix': 'name__startswith',
57
        'scope': 'scope'
58
    }
59
60
    def __init__(self):
61
        super(KeyValuePairController, self).__init__()
62
        self._coordinator = coordination.get_coordinator()
63
        self.get_one_db_method = self._get_by_name
64
65
    def get_one(self, name, requester_user, scope=FULL_SYSTEM_SCOPE, user=None, decrypt=False):
66
        """
67
            List key by name.
68
69
            Handle:
70
                GET /keys/key1
71
        """
72
        if not scope:
73
            scope = FULL_SYSTEM_SCOPE
74
75
        if user:
76
            # Providing a user implies a user scope
77
            scope = FULL_USER_SCOPE
78
79
        if not requester_user:
80
            requester_user = UserDB(cfg.CONF.system_user.user)
81
82
        scope = get_datastore_full_scope(scope)
83
        self._validate_scope(scope=scope)
84
85
        is_admin = rbac_utils.user_is_admin(user_db=requester_user)
86
87
        # User needs to be either admin or requesting item for itself
88
        self._validate_decrypt_query_parameter(decrypt=decrypt, scope=scope, is_admin=is_admin,
89
                                               requester_user=requester_user)
90
91
        user = user or requester_user.name
92
93
        # Validate that the authenticated user is admin if user query param is provided
94
        assert_user_is_admin_if_user_query_param_is_provided(user_db=requester_user,
95
                                                             user=user)
96
97
        key_ref = get_key_reference(scope=scope, name=name, user=user)
98
        from_model_kwargs = {'mask_secrets': not decrypt}
99
        kvp_api = self._get_one_by_scope_and_name(
100
            name=key_ref,
101
            scope=scope,
102
            from_model_kwargs=from_model_kwargs
103
        )
104
105
        return kvp_api
106
107
    def get_all(self, requester_user, prefix=None, scope=FULL_SYSTEM_SCOPE, user=None,
108
                decrypt=False, sort=None, offset=0, limit=None, **raw_filters):
109
        """
110
            List all keys.
111
112
            Handles requests:
113
                GET /keys/
114
        """
115
        if not scope:
116
            scope = FULL_SYSTEM_SCOPE
117
118
        if user:
119
            # Providing a user implies a user scope
120
            scope = FULL_USER_SCOPE
121
122
        if not requester_user:
123
            requester_user = UserDB(cfg.CONF.system_user.user)
124
125
        scope = get_datastore_full_scope(scope)
126
        is_all_scope = (scope == ALL_SCOPE)
127
128
        is_admin = rbac_utils.user_is_admin(user_db=requester_user)
129
        if is_all_scope and not is_admin:
130
            msg = '"all" scope requires administrator access'
131
            raise AccessDeniedError(message=msg, user_db=requester_user)
132
133
        # User needs to be either admin or requesting items for themselves
134
        self._validate_decrypt_query_parameter(decrypt=decrypt, scope=scope, is_admin=is_admin,
135
                                               requester_user=requester_user)
136
137
        user = user or requester_user.name
138
139
        # Validate that the authenticated user is admin if user query param is provided
140
        assert_user_is_admin_if_user_query_param_is_provided(user_db=requester_user,
141
                                                             user=user)
142
143
        from_model_kwargs = {'mask_secrets': not decrypt}
144
145
        if scope and scope not in ALL_SCOPE:
146
            self._validate_scope(scope=scope)
147
            raw_filters['scope'] = scope
148
149
        if scope == USER_SCOPE or scope == FULL_USER_SCOPE:
150
            # Make sure we only returned values scoped to current user
151
            if prefix:
152
                prefix = get_key_reference(name=prefix, scope=scope, user=user)
153
            else:
154
                prefix = get_key_reference(name='', scope=scope, user=user)
155
156
        raw_filters['prefix'] = prefix
157
158
        kvp_apis = super(KeyValuePairController, self)._get_all(from_model_kwargs=from_model_kwargs,
159
                                                                sort=sort,
160
                                                                offset=offset,
161
                                                                limit=limit,
162
                                                                raw_filters=raw_filters,
163
                                                                requester_user=requester_user)
164
        return kvp_apis
165
166
    def put(self, kvp, name, requester_user, scope=FULL_SYSTEM_SCOPE):
167
        """
168
        Create a new entry or update an existing one.
169
        """
170
        if not scope:
171
            scope = FULL_SYSTEM_SCOPE
172
173
        if not requester_user:
174
            requester_user = UserDB(cfg.CONF.system_user.user)
175
176
        scope = getattr(kvp, 'scope', scope)
177
        scope = get_datastore_full_scope(scope)
178
        self._validate_scope(scope=scope)
179
180
        user = getattr(kvp, 'user', requester_user.name) or requester_user.name
181
182
        # Validate that the authenticated user is admin if user query param is provided
183
        assert_user_is_admin_if_user_query_param_is_provided(user_db=requester_user,
184
                                                             user=user)
185
186
        key_ref = get_key_reference(scope=scope, name=name, user=user)
187
        lock_name = self._get_lock_name_for_key(name=key_ref, scope=scope)
188
        LOG.debug('PUT scope: %s, name: %s', scope, name)
189
        # TODO: Custom permission check since the key doesn't need to exist here
190
191
        # Note: We use lock to avoid a race
192
        with self._coordinator.get_lock(lock_name):
193
            try:
194
                existing_kvp_api = self._get_one_by_scope_and_name(
195
                    scope=scope,
196
                    name=key_ref
197
                )
198
            except StackStormDBObjectNotFoundError:
199
                existing_kvp_api = None
200
201
            kvp.name = key_ref
202
            kvp.scope = scope
203
204
            try:
205
                kvp_db = KeyValuePairAPI.to_model(kvp)
206
207
                if existing_kvp_api:
208
                    kvp_db.id = existing_kvp_api.id
209
210
                kvp_db = KeyValuePair.add_or_update(kvp_db)
211
            except (ValidationError, ValueError) as e:
212
                LOG.exception('Validation failed for key value data=%s', kvp)
213
                abort(http_client.BAD_REQUEST, str(e))
214
                return
215
            except CryptoKeyNotSetupException as e:
216
                LOG.exception(str(e))
217
                abort(http_client.BAD_REQUEST, str(e))
218
                return
219
            except InvalidScopeException as e:
220
                LOG.exception(str(e))
221
                abort(http_client.BAD_REQUEST, str(e))
222
                return
223
        extra = {'kvp_db': kvp_db}
224
        LOG.audit('KeyValuePair updated. KeyValuePair.id=%s' % (kvp_db.id), extra=extra)
225
226
        kvp_api = KeyValuePairAPI.from_model(kvp_db)
227
        return kvp_api
228
229
    def delete(self, name, requester_user, scope=FULL_SYSTEM_SCOPE, user=None):
230
        """
231
            Delete the key value pair.
232
233
            Handles requests:
234
                DELETE /keys/1
235
        """
236
        if not scope:
237
            scope = FULL_SYSTEM_SCOPE
238
239
        if not requester_user:
240
            requester_user = UserDB(cfg.CONF.system_user.user)
241
242
        scope = get_datastore_full_scope(scope)
243
        self._validate_scope(scope=scope)
244
245
        user = user or requester_user.name
246
247
        # Validate that the authenticated user is admin if user query param is provided
248
        assert_user_is_admin_if_user_query_param_is_provided(user_db=requester_user,
249
                                                             user=user)
250
251
        key_ref = get_key_reference(scope=scope, name=name, user=user)
252
        lock_name = self._get_lock_name_for_key(name=key_ref, scope=scope)
253
254
        # Note: We use lock to avoid a race
255
        with self._coordinator.get_lock(lock_name):
256
            from_model_kwargs = {'mask_secrets': True}
257
            kvp_api = self._get_one_by_scope_and_name(
258
                name=key_ref,
259
                scope=scope,
260
                from_model_kwargs=from_model_kwargs
261
            )
262
263
            kvp_db = KeyValuePairAPI.to_model(kvp_api)
264
265
            LOG.debug('DELETE /keys/ lookup with scope=%s name=%s found object: %s',
266
                      scope, name, kvp_db)
267
268
            try:
269
                KeyValuePair.delete(kvp_db)
270
            except Exception as e:
271
                LOG.exception('Database delete encountered exception during '
272
                              'delete of name="%s". ', name)
273
                abort(http_client.INTERNAL_SERVER_ERROR, str(e))
274
                return
275
276
        extra = {'kvp_db': kvp_db}
277
        LOG.audit('KeyValuePair deleted. KeyValuePair.id=%s' % (kvp_db.id), extra=extra)
278
279
        return Response(status=http_client.NO_CONTENT)
280
281
    def _get_lock_name_for_key(self, name, scope=FULL_SYSTEM_SCOPE):
282
        """
283
        Retrieve a coordination lock name for the provided datastore item name.
284
285
        :param name: Datastore item name (PK).
286
        :type name: ``str``
287
        """
288
        lock_name = six.b('kvp-crud-%s.%s' % (scope, name))
289
        return lock_name
290
291
    def _validate_decrypt_query_parameter(self, decrypt, scope, is_admin, requester_user):
292
        """
293
        Validate that the provider user is either admin or requesting to decrypt value for
294
        themselves.
295
        """
296
        is_user_scope = (scope == USER_SCOPE or scope == FULL_USER_SCOPE)
297
        if decrypt and (not is_user_scope and not is_admin):
298
            msg = 'Decrypt option requires administrator access'
299
            raise AccessDeniedError(message=msg, user_db=requester_user)
300
301
    def _validate_scope(self, scope):
302
        if scope not in ALLOWED_SCOPES:
303
            msg = 'Scope %s is not in allowed scopes list: %s.' % (scope, ALLOWED_SCOPES)
304
            raise ValueError(msg)
305
306
307
key_value_pair_controller = KeyValuePairController()
308