Passed
Push — master ( 09382a...fb2c82 )
by Plexxi
03:10
created

KeyValuePairAPI.from_model()   F

Complexity

Conditions 13

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
c 0
b 0
f 0
dl 0
loc 31
rs 2.7716

How to fix   Complexity   

Complexity

Complex classes like KeyValuePairAPI.from_model() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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 os
17
import copy
18
import datetime
19
20
from oslo_config import cfg
21
import six
22
23
from st2common.constants.keyvalue import FULL_SYSTEM_SCOPE, FULL_USER_SCOPE, ALLOWED_SCOPES
24
from st2common.constants.keyvalue import SYSTEM_SCOPE, USER_SCOPE
25
from st2common.exceptions.keyvalue import CryptoKeyNotSetupException, InvalidScopeException
26
from st2common.log import logging
27
from st2common.util import isotime
28
from st2common.util import date as date_utils
29
from st2common.util.crypto import read_crypto_key, symmetric_encrypt, symmetric_decrypt
30
from st2common.models.api.base import BaseAPI
31
from st2common.models.system.keyvalue import UserKeyReference
32
from st2common.models.db.keyvalue import KeyValuePairDB
33
34
__all__ = [
35
    'KeyValuePairAPI',
36
    'KeyValuePairSetAPI'
37
]
38
39
LOG = logging.getLogger(__name__)
40
41
42
class KeyValuePairAPI(BaseAPI):
43
    crypto_setup = False
44
    model = KeyValuePairDB
45
    schema = {
46
        'type': 'object',
47
        'properties': {
48
            'id': {
49
                'type': 'string'
50
            },
51
            "uid": {
52
                "type": "string"
53
            },
54
            'name': {
55
                'type': 'string'
56
            },
57
            'description': {
58
                'type': 'string'
59
            },
60
            'value': {
61
                'type': 'string',
62
                'required': True
63
            },
64
            'secret': {
65
                'type': 'boolean',
66
                'required': False,
67
                'default': False
68
            },
69
            'encrypted': {
70
                'type': 'boolean',
71
                'required': False,
72
                'default': False
73
            },
74
            'scope': {
75
                'type': 'string',
76
                'required': False,
77
                'default': FULL_SYSTEM_SCOPE
78
            },
79
            'expire_timestamp': {
80
                'type': 'string',
81
                'pattern': isotime.ISO8601_UTC_REGEX
82
            },
83
            # Note: Those values are only used for input
84
            # TODO: Improve
85
            'ttl': {
86
                'type': 'integer'
87
            }
88
        },
89
        'additionalProperties': False
90
    }
91
92
    @staticmethod
93
    def _setup_crypto():
94
        if KeyValuePairAPI.crypto_setup:
95
            # Crypto already set up
96
            return
97
98
        LOG.info('Checking if encryption is enabled for key-value store.')
99
        KeyValuePairAPI.is_encryption_enabled = cfg.CONF.keyvalue.enable_encryption
100
        LOG.debug('Encryption enabled? : %s', KeyValuePairAPI.is_encryption_enabled)
101
        if KeyValuePairAPI.is_encryption_enabled:
102
            KeyValuePairAPI.crypto_key_path = cfg.CONF.keyvalue.encryption_key_path
103
            LOG.info('Encryption enabled. Looking for key in path %s',
104
                     KeyValuePairAPI.crypto_key_path)
105
            if not os.path.exists(KeyValuePairAPI.crypto_key_path):
106
                msg = ('Encryption key file does not exist in path %s.' %
107
                       KeyValuePairAPI.crypto_key_path)
108
                LOG.exception(msg)
109
                LOG.info('All API requests will now send out BAD_REQUEST ' +
110
                         'if you ask to store secrets in key value store.')
111
                KeyValuePairAPI.crypto_key = None
112
            else:
113
                KeyValuePairAPI.crypto_key = read_crypto_key(
114
                    key_path=KeyValuePairAPI.crypto_key_path
115
                )
116
        KeyValuePairAPI.crypto_setup = True
117
118
    @classmethod
119
    def from_model(cls, model, mask_secrets=True):
120
        if not KeyValuePairAPI.crypto_setup:
121
            KeyValuePairAPI._setup_crypto()
122
123
        doc = cls._from_model(model, mask_secrets=mask_secrets)
124
125
        if getattr(model, 'expire_timestamp', None) and model.expire_timestamp:
126
            doc['expire_timestamp'] = isotime.format(model.expire_timestamp, offset=False)
127
128
        encrypted = False
129
        secret = getattr(model, 'secret', False)
130
        if secret:
131
            encrypted = True
132
133
        if not mask_secrets and secret:
134
            doc['value'] = symmetric_decrypt(KeyValuePairAPI.crypto_key, model.value)
135
            encrypted = False
136
137
        scope = getattr(model, 'scope', SYSTEM_SCOPE)
138
        if scope:
139
            doc['scope'] = scope
140
141
        key = doc.get('name', None)
142
        if (scope == USER_SCOPE or scope == FULL_USER_SCOPE) and key:
143
            doc['user'] = UserKeyReference.get_user(key)
144
            doc['name'] = UserKeyReference.get_name(key)
145
146
        doc['encrypted'] = encrypted
147
        attrs = {attr: value for attr, value in six.iteritems(doc) if value is not None}
148
        return cls(**attrs)
149
150
    @classmethod
151
    def to_model(cls, kvp):
152
        if not KeyValuePairAPI.crypto_setup:
153
            KeyValuePairAPI._setup_crypto()
154
155
        kvp_id = getattr(kvp, 'id', None)
156
        name = getattr(kvp, 'name', None)
157
        description = getattr(kvp, 'description', None)
158
        value = kvp.value
159
        secret = False
160
161
        if getattr(kvp, 'ttl', None):
162
            expire_timestamp = (date_utils.get_datetime_utc_now() +
163
                                datetime.timedelta(seconds=kvp.ttl))
164
        else:
165
            expire_timestamp = None
166
167
        secret = getattr(kvp, 'secret', False)
168
169
        if secret:
170
            if not KeyValuePairAPI.crypto_key:
171
                msg = ('Crypto key not found in %s. Unable to encrypt value for key %s.' %
172
                       (KeyValuePairAPI.crypto_key_path, name))
173
                raise CryptoKeyNotSetupException(msg)
174
            value = symmetric_encrypt(KeyValuePairAPI.crypto_key, value)
175
176
        scope = getattr(kvp, 'scope', FULL_SYSTEM_SCOPE)
177
178
        if scope not in ALLOWED_SCOPES:
179
            raise InvalidScopeException('Invalid scope "%s"! Allowed scopes are %s.' % (
180
                scope, ALLOWED_SCOPES)
181
            )
182
183
        model = cls.model(id=kvp_id, name=name, description=description, value=value,
184
                          secret=secret, scope=scope,
185
                          expire_timestamp=expire_timestamp)
186
187
        return model
188
189
190
class KeyValuePairSetAPI(KeyValuePairAPI):
191
    """
192
    API model for key value set operations.
193
    """
194
195
    schema = copy.deepcopy(KeyValuePairAPI.schema)
196
    schema['properties']['ttl'] = {
197
        'description': 'Items TTL',
198
        'type': 'integer'
199
    }
200
    schema['properties']['user'] = {
201
        'description': ('User to which the value should be scoped to. Only applicable to '
202
                        'scope == user'),
203
        'type': 'string',
204
        'default': None
205
    }
206