Passed
Push — develop ( b7e2c3...d0345b )
by Plexxi
08:11 queued 04:18
created

KeyValuePairAPI._read_crypto_key()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 2
dl 0
loc 5
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
import datetime
17
import os
18
19
from keyczar.keys import AesKey
20
from oslo_config import cfg
21
import six
22
23
from st2common.exceptions.keyvalue import CryptoKeyNotSetupException
24
from st2common.log import logging
25
from st2common.util import isotime
26
from st2common.util import date as date_utils
27
from st2common.util.crypto import symmetric_encrypt, symmetric_decrypt
28
from st2common.models.api.base import BaseAPI
29
from st2common.models.db.keyvalue import KeyValuePairDB
30
31
LOG = logging.getLogger(__name__)
32
33
34
class KeyValuePairAPI(BaseAPI):
35
    crypto_setup = False
36
    model = KeyValuePairDB
37
    schema = {
38
        'type': 'object',
39
        'properties': {
40
            'id': {
41
                'type': 'string'
42
            },
43
            "uid": {
44
                "type": "string"
45
            },
46
            'name': {
47
                'type': 'string'
48
            },
49
            'description': {
50
                'type': 'string'
51
            },
52
            'value': {
53
                'type': 'string',
54
                'required': True
55
            },
56
            'secret': {
57
                'type': 'boolean',
58
                'required': False,
59
                'default': False
60
            },
61
            'encrypted': {
62
                'type': 'boolean',
63
                'required': False,
64
                'default': False
65
            },
66
            'expire_timestamp': {
67
                'type': 'string',
68
                'pattern': isotime.ISO8601_UTC_REGEX
69
            },
70
            # Note: Those values are only used for input
71
            # TODO: Improve
72
            'ttl': {
73
                'type': 'integer'
74
            }
75
        },
76
        'additionalProperties': False
77
    }
78
79
    @staticmethod
80
    def _setup_crypto():
81
        LOG.info('Checking if encryption is enabled for key-value store.')
82
        KeyValuePairAPI.is_encryption_enabled = cfg.CONF.keyvalue.enable_encryption
83
        LOG.debug('Encryption enabled? : %s', KeyValuePairAPI.is_encryption_enabled)
84
        if KeyValuePairAPI.is_encryption_enabled:
85
            KeyValuePairAPI.crypto_key_path = cfg.CONF.keyvalue.encryption_key_path
86
            LOG.info('Encryption enabled. Looking for key in path %s',
87
                     KeyValuePairAPI.crypto_key_path)
88
            if not os.path.exists(KeyValuePairAPI.crypto_key_path):
89
                msg = ('Encryption key file does not exist in path %s.' %
90
                       KeyValuePairAPI.crypto_key_path)
91
                LOG.exception(msg)
92
                LOG.info('All API requests will now send out BAD_REQUEST ' +
93
                         'if you ask to store secrets in key value store.')
94
                KeyValuePairAPI.crypto_key = None
95
            else:
96
                KeyValuePairAPI.crypto_key = KeyValuePairAPI._read_crypto_key(
97
                    KeyValuePairAPI.crypto_key_path
98
                )
99
        KeyValuePairAPI.crypto_setup = True
100
101
    @staticmethod
102
    def _read_crypto_key(key_path):
103
        with open(key_path) as key_file:
104
            key = AesKey.Read(key_file.read())
105
            return key
106
107
    @classmethod
108
    def from_model(cls, model, mask_secrets=True):
109
        if not KeyValuePairAPI.crypto_setup:
110
            KeyValuePairAPI._setup_crypto()
111
112
        doc = cls._from_model(model, mask_secrets=mask_secrets)
113
114
        if 'id' in doc:
115
            del doc['id']
116
117
        if model.expire_timestamp:
118
            doc['expire_timestamp'] = isotime.format(model.expire_timestamp, offset=False)
119
120
        encrypted = False
121
        if model.secret:
122
            encrypted = True
123
124
        if not mask_secrets and model.secret:
125
            doc['value'] = symmetric_decrypt(KeyValuePairAPI.crypto_key, model.value)
126
            encrypted = False
127
128
        doc['encrypted'] = encrypted
129
        attrs = {attr: value for attr, value in six.iteritems(doc) if value is not None}
130
        return cls(**attrs)
131
132
    @classmethod
133
    def to_model(cls, kvp):
134
        if not KeyValuePairAPI.crypto_setup:
135
            KeyValuePairAPI._setup_crypto()
136
137
        name = getattr(kvp, 'name', None)
138
        description = getattr(kvp, 'description', None)
139
        value = kvp.value
140
        secret = False
141
142
        if getattr(kvp, 'ttl', None):
143
            expire_timestamp = (date_utils.get_datetime_utc_now() +
144
                                datetime.timedelta(seconds=kvp.ttl))
145
        else:
146
            expire_timestamp = None
147
148
        if getattr(kvp, 'secret', False):
149
            if not KeyValuePairAPI.crypto_key:
150
                msg = ('Crypto key not found in %s. Unable to encrypt value for key %s.' %
151
                       (KeyValuePairAPI.crypto_key_path, name))
152
                raise CryptoKeyNotSetupException(msg)
153
            value = symmetric_encrypt(KeyValuePairAPI.crypto_key, value)
154
            secret = True
155
156
        model = cls.model(name=name, description=description, value=value,
157
                          secret=secret,
158
                          expire_timestamp=expire_timestamp)
159
160
        return model
161