Passed
Push — master ( 17ab74...ddaae1 )
by Plexxi
03:47
created

KeyValuePairController.get_all()   D

Complexity

Conditions 8

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

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