Completed
Pull Request — master (#2669)
by Lakshmi
06:41
created

KeyValuePairController.get_all()   A

Complexity

Conditions 3

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 3
c 6
b 0
f 0
dl 0
loc 18
rs 9.4285
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
from pecan import abort, expose
17
import six
18
from mongoengine import ValidationError
19
20
from st2api.controllers.resource import ResourceController
21
from st2common import log as logging
22
from st2common.constants.keyvalue import USER_SCOPE, ALLOWED_SCOPES
23
from st2common.exceptions.keyvalue import CryptoKeyNotSetupException, InvalidScopeException
24
from st2common.models.api.keyvalue import KeyValuePairAPI
25
from st2common.models.api.base import jsexpose
26
from st2common.persistence.keyvalue import KeyValuePair
27
from st2common.services import coordination
28
from st2common.services.keyvalues import get_key_reference
29
from st2common.util.api import get_requester
30
31
http_client = six.moves.http_client
32
33
LOG = logging.getLogger(__name__)
34
35
__all__ = [
36
    'ScopedKeyValuePairController'
37
]
38
39
40
class ScopedKeyValuePairController(object):
41
    """
42
    Implements the REST endpoint for managing the scoped key value store.
43
    """
44
45
    @expose()
46
    def _lookup(self, *remainder):
47
        LOG.debug('ScopedKeyValuePairController - Validate scope')
48
        if not remainder:
49
            abort(http_client.BAD_REQUEST, 'No scope specified.')
50
            return
51
        scope = remainder[0]
52
        if not self._is_allowed_scope(scope):
53
            msg = 'Scope %s is not in allowed scopes list: %s.' % (scope, ALLOWED_SCOPES)
54
            abort(http_client.BAD_REQUEST, msg)
55
            return
56
        LOG.info('Got a valid scope. Now should route to key value controller.')
57
        return KeyValuePairController(), remainder
58
59
    @staticmethod
60
    def _is_allowed_scope(scope):
61
        return scope in ALLOWED_SCOPES
62
63
64
class KeyValuePairController(ResourceController):
65
    """
66
    Implements the REST endpoint for managing the key value store.
67
    """
68
69
    model = KeyValuePairAPI
70
    access = KeyValuePair
71
    supported_filters = {
72
        'prefix': 'name__startswith',
73
        'scope': 'scope'
74
    }
75
76
    def __init__(self):
77
        super(KeyValuePairController, self).__init__()
78
        self._coordinator = coordination.get_coordinator()
79
        self.get_one_db_method = self._get_by_name
80
81
    @jsexpose(arg_types=[str, str, str, bool])
82
    def get(self, scope, name=None, prefix=None, decrypt=False, **kwargs):
83
        if scope and name:
84
            return self.get_one(scope=scope, name=name, decrypt=decrypt)
85
86
        if scope and not name:
87
            return self.get_all(scope=scope, name=name, decrypt=decrypt, prefix=prefix, **kwargs)
88
89
    # @jsexpose(arg_types=[str, str, bool])
90
    def get_one(self, scope, name=None, decrypt=False):
0 ignored issues
show
Bug introduced by
Arguments number differs from overridden 'get_one' method
Loading history...
91
        """
92
            List key by name.
93
94
            Handle:
95
                GET /keys/${scope}/key1
96
        """
97
        key_ref = get_key_reference(scope=scope, name=name, user=get_requester())
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
        if not kvp_api:
106
            msg = 'Key with name: %s and scope: %s not found!' % (name, scope)
107
            abort(http_client.NOT_FOUND, msg)
108
            return
109
110
        return kvp_api
111
112
    # @jsexpose(arg_types=[str, str, bool])
113
    def get_all(self, scope, prefix=None, decrypt=False, **kwargs):
114
        """
115
            List all keys.
116
117
            Handles requests:
118
                GET /keys/${scope}
119
        """
120
        from_model_kwargs = {'mask_secrets': not decrypt}
121
        kwargs['prefix'] = prefix
122
        kwargs['scope'] = scope
123
124
        if kwargs['prefix'] and scope == USER_SCOPE:
125
            kwargs['prefix'] = get_key_reference(name=kwargs['prefix'], scope=scope,
126
                                                 user=get_requester())
127
128
        kvp_dbs = super(KeyValuePairController, self)._get_all(from_model_kwargs=from_model_kwargs,
129
                                                               **kwargs)
130
        return kvp_dbs
131
132
    @jsexpose(arg_types=[str, str, str], body_cls=KeyValuePairAPI)
133
    def put(self, scope, name, kvp):
134
        """
135
        Create a new entry or update an existing one.
136
137
        Handles requests:
138
            PUT /keys/${scope}/1
139
        """
140
141
        body_scope = getattr(kvp, 'scope', None)
142
        if body_scope and scope != body_scope:
143
            msg = ('Scope "%s" in URL doesn not match scope "%s" field in body.' % (
144
                   scope, body_scope))
145
            abort(http_client.BAD_REQUEST, msg)
146
            return
147
148
        key_ref = get_key_reference(scope=scope, name=name, user=get_requester())
149
        lock_name = self._get_lock_name_for_key(scope=scope, name=key_ref)
150
151
        # TODO: Custom permission check since the key doesn't need to exist here
152
153
        # Note: We use lock to avoid a race
154
        with self._coordinator.get_lock(lock_name):
155
            existing_kvp_api = self._get_one_by_scope_and_name(
156
                scope=scope,
157
                name=key_ref
158
            )
159
160
            kvp.name = key_ref
161
            kvp.scope = scope
162
163
            try:
164
                kvp_db = KeyValuePairAPI.to_model(kvp)
165
166
                if existing_kvp_api:
167
                    kvp_db.id = existing_kvp_api.id
168
169
                kvp_db = KeyValuePair.add_or_update(kvp_db)
170
            except (ValidationError, ValueError) as e:
171
                LOG.exception('Validation failed for key value data=%s', kvp)
172
                abort(http_client.BAD_REQUEST, str(e))
173
                return
174
            except CryptoKeyNotSetupException as e:
175
                LOG.exception(str(e))
176
                abort(http_client.BAD_REQUEST, str(e))
177
                return
178
            except InvalidScopeException as e:
179
                LOG.exception(str(e))
180
                abort(http_client.BAD_REQUEST, str(e))
181
                return
182
        extra = {'kvp_db': kvp_db}
183
        LOG.audit('KeyValuePair updated. KeyValuePair.id=%s' % (kvp_db.id), extra=extra)
184
185
        kvp_api = KeyValuePairAPI.from_model(kvp_db)
186
        return kvp_api
187
188
    @jsexpose(arg_types=[str, str], status_code=http_client.NO_CONTENT)
189
    def delete(self, scope, name):
190
        """
191
            Delete the key value pair.
192
193
            Handles requests:
194
                DELETE /keys/${scope}/1
195
        """
196
        key_ref = get_key_reference(scope=scope, name=name, user=get_requester())
197
        lock_name = self._get_lock_name_for_key(name=key_ref, scope=scope)
198
199
        # Note: We use lock to avoid a race
200
        with self._coordinator.get_lock(lock_name):
201
            from_model_kwargs = {'mask_secrets': True}
202
            kvp_api = self._get_one_by_scope_and_name(
203
                name=key_ref,
204
                scope=scope,
205
                from_model_kwargs=from_model_kwargs
206
            )
207
208
            if not kvp_api:
209
                abort(http_client.NOT_FOUND)
210
                return
211
212
            kvp_db = KeyValuePairAPI.to_model(kvp_api)
213
214
            LOG.debug('DELETE /keys/ lookup with name=%s found object: %s', name, kvp_db)
215
216
            try:
217
                KeyValuePair.delete(kvp_db)
218
            except Exception as e:
219
                LOG.exception('Database delete encountered exception during '
220
                              'delete of name="%s". ', name)
221
                abort(http_client.INTERNAL_SERVER_ERROR, str(e))
222
                return
223
224
        extra = {'kvp_db': kvp_db}
225
        LOG.audit('KeyValuePair deleted. KeyValuePair.id=%s' % (kvp_db.id), extra=extra)
226
227
    @staticmethod
228
    def _get_lock_name_for_key(scope, name):
229
        """
230
        Retrieve a coordination lock name for the provided datastore item name.
231
232
        :param name: Datastore item name (PK).
233
        :type name: ``str``
234
        """
235
        lock_name = 'kvp-crud-%s.%s' % (scope, name)
236
        return lock_name
237