Passed
Push — develop ( f78e74...17ab74 )
by Plexxi
07:06 queued 03:26
created

KeyValuePairController.get_all()   C

Complexity

Conditions 7

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

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