srikanth | 116e6e8 | 2014-08-19 07:22:37 -0700 | [diff] [blame] | 1 | # |
| 2 | # Copyright (c) 2013 Big Switch Networks, Inc. |
| 3 | # |
| 4 | # Licensed under the Eclipse Public License, Version 1.0 (the |
| 5 | # "License"); you may not use this file except in compliance with the |
| 6 | # License. You may obtain a copy of the License at |
| 7 | # |
| 8 | # http://www.eclipse.org/legal/epl-v10.html |
| 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 |
| 13 | # implied. See the License for the specific language governing |
| 14 | # permissions and limitations under the License. |
| 15 | # |
| 16 | |
| 17 | import sys, re |
| 18 | from django.conf import settings |
| 19 | from django.contrib.auth.views import login |
| 20 | from django.http import HttpResponseRedirect, HttpResponse |
| 21 | from django.utils import simplejson |
| 22 | from django.contrib import auth |
| 23 | from django.contrib.auth.models import User, AnonymousUser |
| 24 | |
| 25 | from .utils import isCloudBuild |
| 26 | from .models import CustomerUser, Customer, Cluster, AuthToken |
| 27 | |
| 28 | import logging |
| 29 | debugLevel = logging.INFO |
| 30 | logfile = None |
| 31 | |
| 32 | # For testing, uncomment as required |
| 33 | #debugLevel = logging.DEBUG |
| 34 | #logfile = 'middleware.log' |
| 35 | |
| 36 | def initLogger(): |
| 37 | logger = logging.getLogger('middleware') |
| 38 | formatter = logging.Formatter('%(asctime)s [%(name)s] %(levelname)s %(message)s') |
| 39 | logger.setLevel(debugLevel) |
| 40 | |
| 41 | # Add a file handler |
| 42 | if logfile: |
| 43 | file_handler = logging.FileHandler(logfile) |
| 44 | file_handler.setFormatter(formatter) |
| 45 | logger.addHandler(file_handler) |
| 46 | |
| 47 | # Add a console handler |
| 48 | console_handler = logging.StreamHandler() |
| 49 | console_handler.setFormatter(formatter) |
| 50 | console_handler.setLevel(debugLevel) |
| 51 | logger.addHandler(console_handler) |
| 52 | return logger |
| 53 | |
| 54 | logger = initLogger() |
| 55 | |
| 56 | def is_localhost(request): |
| 57 | return request.META['REMOTE_ADDR'] in ['127.0.0.1', '127.0.1.1', 'localhost'] |
| 58 | |
| 59 | class RequireAuthMiddleware(object): |
| 60 | """RequireAuthMiddleware: Middleware to enforce authentication |
| 61 | |
| 62 | If it is enabled, every Django-powered page, except LOGIN_URL and the list of EXEMPT_URLS, |
| 63 | will require authentication |
| 64 | |
| 65 | Unautenticated user requests are redirected to the login page set (LOGIN_URL in settings) |
| 66 | Unautenticated REST calls which are returned a JSON error as a 403 forbidden response |
| 67 | """ |
| 68 | |
| 69 | def __init__(self): |
| 70 | self.enforce_auth = isCloudBuild() # For now, enforce authentication only if we are a cloud instance |
| 71 | self.login_url = getattr(settings, 'LOGIN_URL', '/accounts/login/') |
| 72 | self.exempt_urls = [self.login_url] |
| 73 | self.exempt_urls += getattr(settings, 'EXEMPT_URLS', [] ) |
| 74 | self.rest_prefix = getattr(settings, 'REST_PREFIX', '/rest/') |
| 75 | if self.enforce_auth: |
| 76 | logger.info('RequireAuthMiddleware: Enforcing Authentication') |
| 77 | |
| 78 | def process_request(self, request): |
| 79 | if self.enforce_auth: |
| 80 | if is_localhost(request): |
| 81 | return None |
| 82 | if request.user.is_anonymous(): |
| 83 | for url in self.exempt_urls: |
| 84 | if request.path.startswith(url): |
| 85 | return None |
| 86 | return self.redirect(request) |
| 87 | return None |
| 88 | |
| 89 | def process_response(self, request, response): |
| 90 | if request.path.startswith(self.login_url): |
| 91 | response['x-bsc-auth-status'] = 'required' |
| 92 | return response |
| 93 | |
| 94 | def redirect(self, request): |
| 95 | if self.rest_prefix in request.path: |
| 96 | logger.warn('RequireAuthMiddleware: Unauthenticated REST request: %s, %s, %s' % ( |
| 97 | request.path, str(request.user), request.META['REMOTE_ADDR'])) |
| 98 | json_content_type = 'application/json' |
| 99 | json_error_response = {'error_type': 'auth', 'description': 'Authentication error'} |
| 100 | return HttpResponse(simplejson.dumps(json_error_response), json_content_type, 403) |
| 101 | |
| 102 | logger.debug('RequireAuthMiddleware: Redirecting to login page: %s, %s, %s' % ( |
| 103 | request.path, str(request.user), request.META['REMOTE_ADDR'])) |
| 104 | return HttpResponseRedirect('%s?next=%s' % (self.login_url, request.path)) |
| 105 | |
| 106 | |
| 107 | class ClusterAuthenticate(object): |
| 108 | """ClusterAuthenticate: Middleware that authenticates/sets credentials for the REST requests |
| 109 | |
| 110 | If this is a REST request: |
| 111 | If request is from localhost |
| 112 | authorize it and set the user as admin |
| 113 | If a the user already has an authenticated session |
| 114 | validate that this user is authorized to access the requested cluster |
| 115 | If it is from an anonynous user: |
| 116 | extract auth token from the request parameters and |
| 117 | verify that the token is in authorized for the requested cluster |
| 118 | (for now, just validate that the user associated with the token |
| 119 | is authorized to access the requested cluster. In future, we may do more) |
| 120 | """ |
| 121 | |
| 122 | token_param_name = 'auth-token' # Name of the auth token parameter in the query string |
| 123 | enforce_auth = isCloudBuild() # For now, enfore auth only for the cloud instance |
| 124 | bypass_localhost_check = False # Set to true to force auth token check even on localhost |
| 125 | |
| 126 | def process_request(self, request): |
| 127 | logger.debug('url: ' + request.path) |
| 128 | if not self.enforce_auth: |
| 129 | logger.debug('ClusterAuthenticate ignored: is disabled') |
| 130 | return |
| 131 | if is_localhost(request) and not self.bypass_localhost_check: |
| 132 | logger.debug('ClusterAuthenticate ignored: is local request') |
| 133 | return |
| 134 | if self.get_req_cluster_name(request) is None: |
| 135 | logger.debug('ClusterAuthenticate ignored: no cluster name in request'); |
| 136 | return |
| 137 | |
| 138 | # Find user and autorized clusters |
| 139 | cluster_name = self.get_req_cluster_name(request) |
| 140 | cluster = self.get_cluster_from_name(cluster_name) |
| 141 | user = AnonymousUser() |
| 142 | if hasattr(request, 'session'): |
| 143 | user = auth.get_user(request) |
| 144 | logger.debug('User from request: ' + str(user)) |
| 145 | if user.is_authenticated(): |
| 146 | allowed_clusters = self.get_allowed_clusters_for_user(user) |
| 147 | else: |
| 148 | token_string = self.get_req_token_string(request) |
| 149 | user = self.get_user_for_token(token_string) |
| 150 | allowed_clusters = self.get_allowed_clusters_for_token(token_string) |
| 151 | logger.debug('Checking authorization for cluster ' + str(cluster) + |
| 152 | ' in cluster list ' + str(allowed_clusters) + |
| 153 | ' for ' + str(user)) |
| 154 | |
| 155 | # Do validation/set user for request |
| 156 | if hasattr(request, 'user'): |
| 157 | request.user = AnonymousUser() |
| 158 | request._cached_user = AnonymousUser() |
| 159 | if cluster and allowed_clusters: |
| 160 | if cluster in allowed_clusters: |
| 161 | request.user = user |
| 162 | request._cached_user = user |
| 163 | return |
| 164 | |
| 165 | def validate_session(self, request): |
| 166 | return True |
| 167 | |
| 168 | def get_req_cluster_name(self, request): |
| 169 | # Find last entry in path, remove parameters |
| 170 | path = request.path |
| 171 | cluster_name = None |
| 172 | try: |
| 173 | pathcomps = path.split('/') |
| 174 | if 'rest' in (pathcomps[1],pathcomps[2]): |
| 175 | cluster_name_idx = 5 |
| 176 | if pathcomps[cluster_name_idx].find(':') > -1: |
| 177 | cluster_name = pathcomps[cluster_name_idx] |
| 178 | except (TypeError, IndexError): |
| 179 | logger.debug('Type error parsing path for customer: ' + str(path)) |
| 180 | return None |
| 181 | except Exception: |
| 182 | logger.debug('Unknown error parsing path for customer: ' + str(path)) |
| 183 | return None |
| 184 | return cluster_name |
| 185 | |
| 186 | def get_cluster_from_name(self, cluster_name): |
| 187 | if cluster_name: |
| 188 | for cluster in Cluster.objects.all(): |
| 189 | if cluster_name == cluster.id: |
| 190 | logger.debug('Mapped request to cluster "%s"' % cluster_name) |
| 191 | return cluster |
| 192 | logger.warn('Failed to map request to cluster: "%s"' % cluster_name) |
| 193 | return None |
| 194 | |
| 195 | def get_req_token_string(self, request): |
| 196 | token_string = None |
| 197 | try: |
| 198 | token_string = request.REQUEST[self.token_param_name] |
| 199 | token_string = token_string.upper() |
| 200 | except KeyError: |
| 201 | logger.debug('No token param in request') |
| 202 | return token_string |
| 203 | |
| 204 | def get_token_from_name(self, token_string): |
| 205 | token = None |
| 206 | if token_string: |
| 207 | try: |
| 208 | token = AuthToken.objects.get(id=token_string) |
| 209 | except AuthToken.DoesNotExist: |
| 210 | logger.debug('Token not found in DB: ' + str(token_string)) |
| 211 | token = None |
| 212 | except Exception: |
| 213 | logger.debug('Auth Tokens not configured in DB? - ' + str(token_string)) |
| 214 | token = None |
| 215 | return token |
| 216 | |
| 217 | def get_user_for_token(self, token_string): |
| 218 | user = AnonymousUser() |
| 219 | token = self.get_token_from_name(token_string) |
| 220 | if token: |
| 221 | user = token.user |
| 222 | return user |
| 223 | |
| 224 | def get_allowed_clusters_for_token(self, token_string): |
| 225 | """Given an auth token, generate the list of clusters for which it gives credentials |
| 226 | |
| 227 | If the cluster entry in the token object is present, use that |
| 228 | If the customer entry in the token object is present, return all clusters for the customer |
| 229 | If the user entry in the token object is present, use that to get a list of clusters. |
| 230 | |
| 231 | In the future, DB changes may provide varying granularity. |
| 232 | """ |
| 233 | |
| 234 | logger.debug('Got token: ' + str(token_string)) |
| 235 | |
| 236 | cluster_list = [] |
| 237 | token = self.get_token_from_name(token_string) |
| 238 | if token: |
| 239 | if token.cluster is not None: |
| 240 | logger.debug('token mapped to cluster ' + str(token.cluster)) |
| 241 | cluster_list = [token.cluster] |
| 242 | elif token.customer is not None: |
| 243 | logger.debug('token mapped to customer ' + str(token.customer)) |
| 244 | cluster_list = Cluster.objects.filter(customer=token.customer) |
| 245 | else: |
| 246 | logger.debug('token mapped to user ' + str(token.user)) |
| 247 | cluster_list = self.get_allowed_clusters_for_user(token.user) |
| 248 | |
| 249 | logger.debug('Returning clust list ' + str(cluster_list)) |
| 250 | return cluster_list |
| 251 | |
| 252 | def get_allowed_clusters_for_user(self, user): |
| 253 | """Given a user, generate the list of clusters for which it gives credentials |
| 254 | Use user to map to a customer (list) and from there to a list of clusters. |
| 255 | """ |
| 256 | |
| 257 | logger.debug('Got user: ' + str(user)) |
| 258 | |
| 259 | cluster_list = [] |
| 260 | if user: |
| 261 | for cust_user in CustomerUser.objects.filter(user=user): |
| 262 | # Currently the query below isn't working. Brute force alternative given |
| 263 | #cluster_list.extend(Cluster.objects.filter(customer=cust_user.customer)) |
| 264 | for cluster in Cluster.objects.all(): |
| 265 | if cluster.customer == cust_user.customer: |
| 266 | cluster_list.append(cluster) |
| 267 | |
| 268 | logger.debug('Returning cluster list ' + str(cluster_list)) |
| 269 | return cluster_list |