blob: f66b3678ec7002095335b3e70d045c042c84fc75 [file] [log] [blame]
srikanth116e6e82014-08-19 07:22:37 -07001#
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
17import sys, re
18from django.conf import settings
19from django.contrib.auth.views import login
20from django.http import HttpResponseRedirect, HttpResponse
21from django.utils import simplejson
22from django.contrib import auth
23from django.contrib.auth.models import User, AnonymousUser
24
25from .utils import isCloudBuild
26from .models import CustomerUser, Customer, Cluster, AuthToken
27
28import logging
29debugLevel = logging.INFO
30logfile = None
31
32# For testing, uncomment as required
33#debugLevel = logging.DEBUG
34#logfile = 'middleware.log'
35
36def 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
54logger = initLogger()
55
56def is_localhost(request):
57 return request.META['REMOTE_ADDR'] in ['127.0.0.1', '127.0.1.1', 'localhost']
58
59class 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
107class 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