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

ScopedKeyValuePairController._lookup()   A

Complexity

Conditions 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
c 2
b 0
f 0
dl 0
loc 10
rs 9.4285
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
0 ignored issues
show
Bug introduced by
There seems to be a cyclic import (st2api.controllers.v1.packs -> st2api.controllers.v1.packviews).

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 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_api = self._get_one_by_scope_and_name(
86
            name=name,
87
            scope=scope,
88
            from_model_kwargs=from_model_kwargs
89
        )
90
91
        if not kvp_api:
92
            msg = 'Key with name: %s and scope: %s not found!' % (name, scope)
93
            abort(http_client.NOT_FOUND, msg)
94
            return
95
96
        return kvp_api
97
98
    @jsexpose(arg_types=[str, str, bool])
99
    def get_all(self, scope, prefix=None, decrypt=False, **kwargs):
100
        """
101
            List all keys.
102
103
            Handles requests:
104
                GET /keys/${scope}
105
        """
106
        from_model_kwargs = {'mask_secrets': not decrypt}
107
        kwargs['prefix'] = prefix
108
        kwargs['scope'] = scope
109
        kvp_dbs = super(KeyValuePairController, self)._get_all(from_model_kwargs=from_model_kwargs,
110
                                                               **kwargs)
111
        return kvp_dbs
112
113
    @jsexpose(arg_types=[str, str, str], body_cls=KeyValuePairAPI)
114
    def put(self, scope, name, kvp):
115
        """
116
        Create a new entry or update an existing one.
117
118
        Handles requests:
119
            PUT /keys/${scope}/1
120
        """
121
122
        body_scope = getattr(kvp, 'scope', None)
123
        if scope != body_scope:
124
            msg = 'URL scope: "%s" doesn\'t match scope field in body: "%s"' % (scope, body_scope)
125
            abort(http_client.BAD_REQUEST, msg)
126
            return
127
128
        lock_name = self._get_lock_name_for_key(name=name)
129
130
        # TODO: Custom permission check since the key doesn't need to exist here
131
132
        # Note: We use lock to avoid a race
133
        with self._coordinator.get_lock(lock_name):
134
            existing_kvp_api = self._get_one_by_scope_and_name(
135
                scope=scope,
136
                name=name
137
            )
138
139
            kvp.name = name
140
            kvp.scope = scope
141
142
            try:
143
                kvp_db = KeyValuePairAPI.to_model(kvp)
144
145
                if existing_kvp_api:
146
                    kvp_db.id = existing_kvp_api.id
147
148
                kvp_db = KeyValuePair.add_or_update(kvp_db)
149
            except (ValidationError, ValueError) as e:
150
                LOG.exception('Validation failed for key value data=%s', kvp)
151
                abort(http_client.BAD_REQUEST, str(e))
152
                return
153
            except CryptoKeyNotSetupException as e:
154
                LOG.exception(str(e))
155
                abort(http_client.BAD_REQUEST, str(e))
156
                return
157
            except InvalidScopeException as e:
158
                LOG.exception(str(e))
159
                abort(http_client.BAD_REQUEST, str(e))
160
                return
161
        extra = {'kvp_db': kvp_db}
162
        LOG.audit('KeyValuePair updated. KeyValuePair.id=%s' % (kvp_db.id), extra=extra)
163
164
        kvp_api = KeyValuePairAPI.from_model(kvp_db)
165
        return kvp_api
166
167
    @jsexpose(arg_types=[str, str], status_code=http_client.NO_CONTENT)
168
    def delete(self, scope, name):
169
        """
170
            Delete the key value pair.
171
172
            Handles requests:
173
                DELETE /keys/1
174
        """
175
        lock_name = self._get_lock_name_for_key(name=name)
176
177
        # Note: We use lock to avoid a race
178
        with self._coordinator.get_lock(lock_name):
179
            from_model_kwargs = {'mask_secrets': True}
180
            kvp_api = self._get_one_by_scope_and_name(
181
                name=name,
182
                scope=scope,
183
                from_model_kwargs=from_model_kwargs
184
            )
185
            kvp_db = KeyValuePairAPI.to_model(kvp_api)
186
187
            if not kvp_db:
188
                abort(http_client.NOT_FOUND)
189
                return
190
191
            LOG.debug('DELETE /keys/ lookup with name=%s found object: %s', name, kvp_db)
192
193
            try:
194
                KeyValuePair.delete(kvp_db)
195
            except Exception as e:
196
                LOG.exception('Database delete encountered exception during '
197
                              'delete of name="%s". ', name)
198
                abort(http_client.INTERNAL_SERVER_ERROR, str(e))
199
                return
200
201
        extra = {'kvp_db': kvp_db}
202
        LOG.audit('KeyValuePair deleted. KeyValuePair.id=%s' % (kvp_db.id), extra=extra)
203
204
    @staticmethod
205
    def _get_lock_name_for_key(name):
206
        """
207
        Retrieve a coordination lock name for the provided datastore item name.
208
209
        :param name: Datastore item name (PK).
210
        :type name: ``str``
211
        """
212
        lock_name = 'kvp-crud-%s' % (name)
213
        return lock_name
214