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

ScopedKeyValuePairController   A

Complexity

Total Complexity 3

Size/Duplication

Total Lines 19
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 19
rs 10
wmc 3

2 Methods

Rating   Name   Duplication   Size   Complexity  
A _lookup() 0 10 2
A _is_allowed_scope() 0 3 1
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
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
        scope = remainder[0]
49
        if not self._is_allowed_scope(scope):
50
            msg = 'Scope %s is not in allowed scopes list: %s.' % (scope, ALLOWED_SCOPES)
51
            abort(http_client.BAD_REQUEST, msg)
52
            return
53
        LOG.info('Got a valid scope. Now should route to key value controller.')
54
        return KeyValuePairController(), remainder
55
56
    @staticmethod
57
    def _is_allowed_scope(scope):
58
        return scope in ALLOWED_SCOPES
59
60
61
class KeyValuePairController(ResourceController):
62
    """
63
    Implements the REST endpoint for managing the key value store.
64
    """
65
66
    model = KeyValuePairAPI
67
    access = KeyValuePair
68
    supported_filters = {
69
        'prefix': 'name__startswith',
70
        'scope': 'scope'
71
    }
72
73
    def __init__(self):
74
        super(KeyValuePairController, self).__init__()
75
        self._coordinator = coordination.get_coordinator()
76
        self.get_one_db_method = self._get_by_name
77
78
    @jsexpose(arg_types=[str, str, bool])
79
    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...
80
        """
81
            List key by name.
82
83
            Handle:
84
                GET /keys/${scope}/key1
85
        """
86
        key_ref = get_key_reference(scope=scope, name=name, user=get_requester())
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
        if not kvp_api:
95
            msg = 'Key with name: %s and scope: %s not found!' % (name, scope)
96
            abort(http_client.NOT_FOUND, msg)
97
            return
98
99
        return kvp_api
100
101
    @jsexpose(arg_types=[str, str, bool])
102
    def get_all(self, scope, prefix=None, decrypt=False, **kwargs):
103
        """
104
            List all keys.
105
106
            Handles requests:
107
                GET /keys/${scope}
108
        """
109
        from_model_kwargs = {'mask_secrets': not decrypt}
110
        kwargs['prefix'] = prefix
111
        kwargs['scope'] = scope
112
        kvp_dbs = super(KeyValuePairController, self)._get_all(from_model_kwargs=from_model_kwargs,
113
                                                               **kwargs)
114
        return kvp_dbs
115
116
    @jsexpose(arg_types=[str, str, str], body_cls=KeyValuePairAPI)
117
    def put(self, scope, name, kvp):
118
        """
119
        Create a new entry or update an existing one.
120
121
        Handles requests:
122
            PUT /keys/${scope}/1
123
        """
124
125
        body_scope = getattr(kvp, 'scope', None)
126
        if scope != body_scope:
127
            msg = 'URL scope: "%s" doesn\'t match scope field in body: "%s"' % (scope, body_scope)
128
            abort(http_client.BAD_REQUEST, msg)
129
            return
130
131
        key_ref = get_key_reference(scope=scope, name=name, user=get_requester())
132
        lock_name = self._get_lock_name_for_key(scope=scope, name=key_ref)
133
134
        # TODO: Custom permission check since the key doesn't need to exist here
135
136
        # Note: We use lock to avoid a race
137
        with self._coordinator.get_lock(lock_name):
138
            existing_kvp_api = self._get_one_by_scope_and_name(
139
                scope=scope,
140
                name=key_ref
141
            )
142
143
            kvp.name = key_ref
144
            kvp.scope = scope
145
146
            try:
147
                kvp_db = KeyValuePairAPI.to_model(kvp)
148
149
                if existing_kvp_api:
150
                    kvp_db.id = existing_kvp_api.id
151
152
                kvp_db = KeyValuePair.add_or_update(kvp_db)
153
            except (ValidationError, ValueError) as e:
154
                LOG.exception('Validation failed for key value data=%s', kvp)
155
                abort(http_client.BAD_REQUEST, str(e))
156
                return
157
            except CryptoKeyNotSetupException as e:
158
                LOG.exception(str(e))
159
                abort(http_client.BAD_REQUEST, str(e))
160
                return
161
            except InvalidScopeException as e:
162
                LOG.exception(str(e))
163
                abort(http_client.BAD_REQUEST, str(e))
164
                return
165
        extra = {'kvp_db': kvp_db}
166
        LOG.audit('KeyValuePair updated. KeyValuePair.id=%s' % (kvp_db.id), extra=extra)
167
168
        kvp_api = KeyValuePairAPI.from_model(kvp_db)
169
        return kvp_api
170
171
    @jsexpose(arg_types=[str, str], status_code=http_client.NO_CONTENT)
172
    def delete(self, scope, name):
173
        """
174
            Delete the key value pair.
175
176
            Handles requests:
177
                DELETE /keys/1
178
        """
179
        key_ref = get_key_reference(scope=scope, name=name, user=get_requester())
180
        lock_name = self._get_lock_name_for_key(name=key_ref, scope=scope)
181
182
        # Note: We use lock to avoid a race
183
        with self._coordinator.get_lock(lock_name):
184
            from_model_kwargs = {'mask_secrets': True}
185
            kvp_api = self._get_one_by_scope_and_name(
186
                name=key_ref,
187
                scope=scope,
188
                from_model_kwargs=from_model_kwargs
189
            )
190
            kvp_db = KeyValuePairAPI.to_model(kvp_api)
191
192
            if not kvp_db:
193
                abort(http_client.NOT_FOUND)
194
                return
195
196
            LOG.debug('DELETE /keys/ lookup with name=%s found object: %s', name, kvp_db)
197
198
            try:
199
                KeyValuePair.delete(kvp_db)
200
            except Exception as e:
201
                LOG.exception('Database delete encountered exception during '
202
                              'delete of name="%s". ', name)
203
                abort(http_client.INTERNAL_SERVER_ERROR, str(e))
204
                return
205
206
        extra = {'kvp_db': kvp_db}
207
        LOG.audit('KeyValuePair deleted. KeyValuePair.id=%s' % (kvp_db.id), extra=extra)
208
209
    @staticmethod
210
    def _get_lock_name_for_key(scope, name):
211
        """
212
        Retrieve a coordination lock name for the provided datastore item name.
213
214
        :param name: Datastore item name (PK).
215
        :type name: ``str``
216
        """
217
        lock_name = 'kvp-crud-%s.%s' % (name)
218
        return lock_name
219