Completed
Pull Request — master (#2920)
by Anthony
04:54
created

TokenController._handle_standalone_auth()   F

Complexity

Conditions 15

Size

Total Lines 95

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
c 1
b 0
f 0
dl 0
loc 95
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like TokenController._handle_standalone_auth() 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 base64
17
18
import pecan
19
from pecan import rest
20
from six.moves import http_client
21
from oslo_config import cfg
22
23
from st2common.exceptions.auth import TokenNotFoundError, TokenExpiredError
24
from st2common.exceptions.auth import TTLTooLargeException, UserNotFoundError
25
from st2common.exceptions.auth import NoNicknameOriginProvidedError, AmbiguousUserError
26
from st2common.exceptions.auth import NotServiceUserError
27
from st2common.models.api.base import jsexpose
28
from st2common.models.api.auth import TokenAPI
29
from st2common.persistence.auth import User
30
from st2common.services.access import create_token
31
from st2common.util import auth as auth_utils
32
from st2common import log as logging
33
from st2auth.backends import get_backend_instance
34
35
36
LOG = logging.getLogger(__name__)
37
38
39
class TokenValidationController(rest.RestController):
40
41
    @jsexpose(body_cls=TokenAPI, status_code=http_client.OK)
42
    def post(self, request, **kwargs):
43
        token = getattr(request, 'token', None)
44
45
        if not token:
46
            pecan.abort(http_client.BAD_REQUEST, 'Token is not provided.')
47
48
        try:
49
            return {'valid': auth_utils.validate_token(token) is not None}
50
        except (TokenNotFoundError, TokenExpiredError):
51
            return {'valid': False}
52
        except Exception:
53
            msg = 'Unexpected error occurred while verifying token.'
54
            LOG.exception(msg)
55
            pecan.abort(http_client.INTERNAL_SERVER_ERROR, msg)
56
57
58
class TokenController(rest.RestController):
59
    validate = TokenValidationController()
60
61
    def __init__(self, *args, **kwargs):
62
        super(TokenController, self).__init__(*args, **kwargs)
63
64
        if cfg.CONF.auth.mode == 'standalone':
65
            self._auth_backend = get_backend_instance(name=cfg.CONF.auth.backend)
66
        else:
67
            self._auth_backend = None
68
69
    @jsexpose(body_cls=TokenAPI, status_code=http_client.CREATED)
70
    def post(self, request, **kwargs):
71
        if cfg.CONF.auth.mode == 'proxy':
72
            return self._handle_proxy_auth(request=request, **kwargs)
73
        elif cfg.CONF.auth.mode == 'standalone':
74
            return self._handle_standalone_auth(request=request, **kwargs)
75
76
    def _handle_proxy_auth(self, request, **kwargs):
77
        remote_addr = pecan.request.headers.get('x-forwarded-for', pecan.request.remote_addr)
78
        extra = {'remote_addr': remote_addr}
79
80
        if pecan.request.remote_user:
81
            ttl = getattr(request, 'ttl', None)
82
            try:
83
                token = self._create_token_for_user(username=pecan.request.remote_user, ttl=ttl)
84
            except TTLTooLargeException as e:
85
                self._abort_request(status_code=http_client.BAD_REQUEST,
86
                                    message=e.message)
87
            return self._process_successful_response(token=token)
88
89
        LOG.audit('Access denied to anonymous user.', extra=extra)
90
        self._abort_request()
91
92
    def _handle_standalone_auth(self, request, **kwargs):
93
        authorization = pecan.request.authorization
94
95
        auth_backend = self._auth_backend.__class__.__name__
96
        remote_addr = pecan.request.remote_addr
97
        extra = {'auth_backend': auth_backend, 'remote_addr': remote_addr}
98
99
        if not authorization:
100
            LOG.audit('Authorization header not provided', extra=extra)
101
            self._abort_request()
102
            return
103
104
        auth_type, auth_value = authorization
105
        if auth_type.lower() not in ['basic']:
106
            extra['auth_type'] = auth_type
107
            LOG.audit('Unsupported authorization type: %s' % (auth_type), extra=extra)
108
            self._abort_request()
109
            return
110
111
        try:
112
            auth_value = base64.b64decode(auth_value)
113
        except Exception:
114
            LOG.audit('Invalid authorization header', extra=extra)
115
            self._abort_request()
116
            return
117
118
        split = auth_value.split(':')
119
        if len(split) != 2:
120
            LOG.audit('Invalid authorization header', extra=extra)
121
            self._abort_request()
122
            return
123
124
        username, password = split
125
        result = self._auth_backend
126
127
        result = self._auth_backend.authenticate(username=username, password=password)
128
        if result is True:
129
            LOG.audit(request.body)
130
            ttl = getattr(request, 'ttl', None)
131
            impersonate_user = getattr(request.body, 'user', None)
132
133
            if impersonate_user is not None:
134
                # check this is a service account
135
                if not User.get_by_name(username).is_service:
136
                    message = "Current user is not a service and cannot " \
137
                              "request impersonated tokens"
138
                    self._abort_request(status_code=http_client.BAD_REQUEST,
139
                                        message=message)
140
                    return
141
                username = impersonate_user
142
            else:
143
                impersonate_user = getattr(request, 'impersonate_user', None)
144
                nickname_origin = getattr(request, 'nickname_origin', None)
145
                if impersonate_user is not None:
146
                    try:
147
                        # check this is a service account
148
                        if not User.get_by_name(username).is_service:
149
                            raise NotServiceUserError()
150
                        username = User.get_by_nickname(impersonate_user,
151
                                                        nickname_origin).name
152
                    except NotServiceUserError:
153
                        message = "Current user is not a service and cannot " \
154
                                  "request impersonated tokens"
155
                        self._abort_request(status_code=http_client.BAD_REQUEST,
156
                                            message=message)
157
                        return
158
                    except UserNotFoundError:
159
                        message = "Could not locate user %s@%s" % \
160
                                  (impersonate_user, nickname_origin)
161
                        self._abort_request(status_code=http_client.BAD_REQUEST,
162
                                            message=message)
163
                        return
164
                    except NoNicknameOriginProvidedError:
165
                        message = "Nickname origin is not provided for nickname '%s'" % \
166
                                  impersonate_user
167
                        self._abort_request(status_code=http_client.BAD_REQUEST,
168
                                            message=message)
169
                        return
170
                    except AmbiguousUserError:
171
                        message = "%s@%s matched more than one username" % \
172
                                  (impersonate_user, nickname_origin)
173
                        self._abort_request(status_code=http_client.BAD_REQUEST,
174
                                            message=message)
175
                        return
176
            try:
177
                token = self._create_token_for_user(
178
                    username=username, ttl=ttl)
179
                return self._process_successful_response(token=token)
180
            except TTLTooLargeException as e:
181
                self._abort_request(status_code=http_client.BAD_REQUEST,
182
                                    message=e.message)
183
                return
184
185
        LOG.audit('Invalid credentials provided', extra=extra)
186
        self._abort_request()
187
188
    def _abort_request(self, status_code=http_client.UNAUTHORIZED,
189
                       message='Invalid or missing credentials'):
190
        pecan.abort(status_code, message)
191
192
    def _process_successful_response(self, token):
193
        api_url = cfg.CONF.auth.api_url
194
        pecan.response.headers['X-API-URL'] = api_url
195
        return token
196
197
    def _create_token_for_user(self, username, ttl=None):
198
        tokendb = create_token(username=username, ttl=ttl)
199
        return TokenAPI.from_model(tokendb)
200