Completed
Pull Request — master (#2920)
by Anthony
05:14
created

TokenController   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 140
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 140
rs 10
wmc 26

7 Methods

Rating   Name   Duplication   Size   Complexity  
A _create_token_for_user() 0 3 1
A __init__() 0 7 2
A _handle_proxy_auth() 0 15 3
F _handle_standalone_auth() 0 93 15
A post() 0 6 3
A _process_successful_response() 0 4 1
A _abort_request() 0 3 1
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
            ttl = getattr(request, 'ttl', None)
130
            impersonate_user = getattr(request, 'user', None)
131
132
            if impersonate_user is not None:
133
                # check this is a service account
134
                if not User.get_by_name(username).is_service:
135
                    message = "Current user is not a service and cannot " \
136
                              "request impersonated tokens"
137
                    self._abort_request(status_code=http_client.BAD_REQUEST,
138
                                        message=message)
139
                    return
140
                username = impersonate_user
141
            else:
142
                impersonate_user = getattr(request, 'impersonate_user', None)
143
                nickname_origin = getattr(request, 'nickname_origin', None)
144
                if impersonate_user is not None:
145
                    try:
146
                        # check this is a service account
147
                        if not User.get_by_name(username).is_service:
148
                            raise NotServiceUserError()
149
                        username = User.get_by_nickname(impersonate_user,
150
                                                        nickname_origin).name
151
                    except NotServiceUserError:
152
                        message = "Current user is not a service and cannot " \
153
                                  "request impersonated tokens"
154
                        self._abort_request(status_code=http_client.BAD_REQUEST,
155
                                            message=message)
156
                        return
157
                    except UserNotFoundError:
158
                        message = "Could not locate user %s@%s" % \
159
                                  (impersonate_user, nickname_origin)
160
                        self._abort_request(status_code=http_client.BAD_REQUEST,
161
                                            message=message)
162
                        return
163
                    except NoNicknameOriginProvidedError:
164
                        message = "Nickname origin is not provided for nickname '%s'" % \
165
                                  impersonate_user
166
                        self._abort_request(status_code=http_client.BAD_REQUEST,
167
                                            message=message)
168
                        return
169
                    except AmbiguousUserError:
170
                        message = "%s@%s matched more than one username" % \
171
                                  (impersonate_user, nickname_origin)
172
                        self._abort_request(status_code=http_client.BAD_REQUEST,
173
                                            message=message)
174
                        return
175
            try:
176
                token = self._create_token_for_user(username=username, ttl=ttl)
177
                return self._process_successful_response(token=token)
178
            except TTLTooLargeException as e:
179
                self._abort_request(status_code=http_client.BAD_REQUEST,
180
                                    message=e.message)
181
                return
182
183
        LOG.audit('Invalid credentials provided', extra=extra)
184
        self._abort_request()
185
186
    def _abort_request(self, status_code=http_client.UNAUTHORIZED,
187
                       message='Invalid or missing credentials'):
188
        pecan.abort(status_code, message)
189
190
    def _process_successful_response(self, token):
191
        api_url = cfg.CONF.auth.api_url
192
        pecan.response.headers['X-API-URL'] = api_url
193
        return token
194
195
    def _create_token_for_user(self, username, ttl=None):
196
        tokendb = create_token(username=username, ttl=ttl)
197
        return TokenAPI.from_model(tokendb)
198