Completed
Push — master ( 4e35fb...c22065 )
by Manas
03:42
created

ApiKeyController.get_one()   A

Complexity

Conditions 3

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
dl 0
loc 23
rs 9.0856
c 1
b 0
f 0
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 pecan
17
import six
18
19
from oslo_config import cfg
20
from pecan import abort
21
from mongoengine import ValidationError
22
23
from st2api.controllers.base import BaseRestControllerMixin, SHOW_SECRETS_QUERY_PARAM
24
from st2common import log as logging
25
from st2common.models.api.auth import ApiKeyAPI, ApiKeyCreateResponseAPI
26
from st2common.models.api.base import jsexpose
27
from st2common.constants.secrets import MASKED_ATTRIBUTE_VALUE
28
from st2common.exceptions.auth import ApiKeyNotFoundError
29
from st2common.persistence.auth import ApiKey
30
from st2common.rbac.types import PermissionType
31
from st2common.rbac.decorators import request_user_has_permission
32
from st2common.rbac.decorators import request_user_has_resource_api_permission
33
from st2common.rbac.decorators import request_user_has_resource_db_permission
34
from st2common.util import auth as auth_util
35
36
http_client = six.moves.http_client
37
38
LOG = logging.getLogger(__name__)
39
40
__all__ = [
41
    'ApiKeyController'
42
]
43
44
45
class ApiKeyController(BaseRestControllerMixin):
46
    """
47
    Implements the REST endpoint for managing the key value store.
48
    """
49
50
    supported_filters = {
51
        'user': 'user'
52
    }
53
54
    query_options = {
55
        'sort': ['user']
56
    }
57
58
    def __init__(self):
59
        super(ApiKeyController, self).__init__()
60
        self.get_one_db_method = ApiKey.get_by_key_or_id
61
62
    @request_user_has_resource_db_permission(permission_type=PermissionType.API_KEY_VIEW)
63
    @jsexpose(arg_types=[str])
64
    def get_one(self, api_key_id_or_key):
65
        """
66
            List api keys.
67
68
            Handle:
69
                GET /apikeys/1
70
        """
71
        api_key_db = None
72
        try:
73
            api_key_db = ApiKey.get_by_key_or_id(api_key_id_or_key)
74
        except ApiKeyNotFoundError:
75
            msg = 'ApiKey matching %s for reference and id not found.', api_key_id_or_key
76
            LOG.exception(msg)
77
            abort(http_client.NOT_FOUND, msg)
78
79
        try:
80
            mask_secrets = self._get_mask_secrets(pecan.request)
81
            return ApiKeyAPI.from_model(api_key_db, mask_secrets=mask_secrets)
82
        except (ValidationError, ValueError) as e:
83
            LOG.exception('Failed to serialize API key.')
84
            abort(http_client.INTERNAL_SERVER_ERROR, str(e))
85
86
    @request_user_has_permission(permission_type=PermissionType.API_KEY_LIST)
87
    @jsexpose(arg_types=[str])
88
    def get_all(self, **kw):
89
        """
90
            List all keys.
91
92
            Handles requests:
93
                GET /keys/
94
        """
95
        mask_secrets, kw = self._get_mask_secrets_ex(**kw)
96
        api_key_dbs = ApiKey.get_all(**kw)
97
        api_keys = [ApiKeyAPI.from_model(api_key_db, mask_secrets=mask_secrets)
98
                    for api_key_db in api_key_dbs]
99
100
        return api_keys
101
102
    @jsexpose(body_cls=ApiKeyAPI, status_code=http_client.CREATED)
103
    @request_user_has_resource_api_permission(permission_type=PermissionType.API_KEY_CREATE)
104
    def post(self, api_key_api):
105
        """
106
        Create a new entry.
107
        """
108
        api_key_db = None
109
        api_key = None
110
        try:
111
            if not getattr(api_key_api, 'user', None):
112
                api_key_api.user = self._get_user()
113
            # If key_hash is provided use that and do not create a new key. The assumption
114
            # is user already has the original api-key
115
            if not getattr(api_key_api, 'key_hash', None):
116
                api_key, api_key_hash = auth_util.generate_api_key_and_hash()
117
                # store key_hash in DB
118
                api_key_api.key_hash = api_key_hash
119
            api_key_db = ApiKey.add_or_update(ApiKeyAPI.to_model(api_key_api))
120
        except (ValidationError, ValueError) as e:
121
            LOG.exception('Validation failed for api_key data=%s.', api_key_api)
122
            abort(http_client.BAD_REQUEST, str(e))
123
124
        extra = {'api_key_db': api_key_db}
125
        LOG.audit('ApiKey created. ApiKey.id=%s' % (api_key_db.id), extra=extra)
126
127
        api_key_create_response_api = ApiKeyCreateResponseAPI.from_model(api_key_db)
128
        # Return real api_key back to user. A one-way hash of the api_key is stored in the DB
129
        # only the real value only returned at create time. Also, no masking of key here since
130
        # the user needs to see this value atleast once.
131
        api_key_create_response_api.key = api_key
132
        return api_key_create_response_api
133
134
    @request_user_has_resource_db_permission(permission_type=PermissionType.API_KEY_MODIFY)
135
    @jsexpose(arg_types=[str], body_cls=ApiKeyAPI)
136
    def put(self, api_key_id_or_key, api_key_api):
137
        api_key_db = ApiKey.get_by_key_or_id(api_key_id_or_key)
138
139
        LOG.debug('PUT /apikeys/ lookup with api_key_id_or_key=%s found object: %s',
140
                  api_key_id_or_key, api_key_db)
141
142
        old_api_key_db = api_key_db
143
        api_key_db = ApiKeyAPI.to_model(api_key_api)
144
145
        # Passing in key_hash as MASKED_ATTRIBUTE_VALUE is expected since we do not
146
        # leak it out therefore it is expected we get the same value back. Interpret
147
        # this special code and empty value as no-change
148
        if api_key_db.key_hash == MASKED_ATTRIBUTE_VALUE or not api_key_db.key_hash:
149
            api_key_db.key_hash = old_api_key_db.key_hash
150
151
        # Rather than silently ignore any update to key_hash it is better to explicitly
152
        # disallow and notify user.
153
        if old_api_key_db.key_hash != api_key_db.key_hash:
154
            raise ValueError('Update of key_hash is not allowed.')
155
156
        api_key_db.id = old_api_key_db.id
157
        api_key_db = ApiKey.add_or_update(api_key_db)
158
159
        extra = {'old_api_key_db': old_api_key_db, 'new_api_key_db': api_key_db}
160
        LOG.audit('API Key updated. ApiKey.id=%s.' % (api_key_db.id), extra=extra)
161
        api_key_api = ApiKeyAPI.from_model(api_key_db)
162
163
        return api_key_api
164
165
    @request_user_has_resource_db_permission(permission_type=PermissionType.API_KEY_DELETE)
166
    @jsexpose(arg_types=[str], status_code=http_client.NO_CONTENT)
167
    def delete(self, api_key_id_or_key):
168
        """
169
            Delete the key value pair.
170
171
            Handles requests:
172
                DELETE /apikeys/1
173
        """
174
        api_key_db = ApiKey.get_by_key_or_id(api_key_id_or_key)
175
176
        LOG.debug('DELETE /apikeys/ lookup with api_key_id_or_key=%s found object: %s',
177
                  api_key_id_or_key, api_key_db)
178
179
        ApiKey.delete(api_key_db)
180
181
        extra = {'api_key_db': api_key_db}
182
        LOG.audit('ApiKey deleted. ApiKey.id=%s' % (api_key_db.id), extra=extra)
183
184
    def _get_user(self):
185
        """
186
        Looks up user from the auth context in the request or will return system_user.
187
        """
188
        # lookup user from request context. AuthHook places context in the pecan request.
189
        auth_context = pecan.request.context.get('auth', None)
190
191
        if not auth_context:
192
            return cfg.CONF.system_user.user
193
194
        user_db = auth_context.get('user', None)
195
        return user_db.name if user_db else cfg.CONF.system_user.user
196
197
    def _get_mask_secrets_ex(self, **kw):
198
        """
199
        Allowing SHOW_SECRETS_QUERY_PARAM to remain in the parameters causes downstream
200
        lookup failures there removing. This is a pretty hackinsh way to manage query params.
201
        """
202
        mask_secrets = self._get_mask_secrets(pecan.request)
203
        kw.pop(SHOW_SECRETS_QUERY_PARAM, None)
204
        return mask_secrets, kw
205