Completed
Pull Request — master (#2669)
by Lakshmi
07:13
created

KeyValuePairController.put()   C

Complexity

Conditions 7

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

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