| 182 | | def get_schema(obj): |
| 183 | | """Return the schema name of the trac environment |
| 184 | | |
| 185 | | """ |
| 186 | | db = obj.env.get_db_cnx() |
| 187 | | schema = db.schema |
| 188 | | db.close() |
| 189 | | del db |
| 190 | | return schema |
| 191 | | |
| 192 | | class LoginModule(Component): |
| 193 | | """Implements user authentication based on a central database |
| 194 | | |
| 195 | | """ |
| 196 | | |
| 197 | | implements(IAuthenticator, INavigationContributor, IRequestHandler) |
| 198 | | |
| 199 | | check_ip = BoolOption('trac', 'check_auth_ip', 'true', |
| 200 | | """Whether the IP address of the user should be checked for |
| 201 | | authentication (''since 0.9'').""") |
| 202 | | |
| 203 | | ignore_case = BoolOption('trac', 'ignore_auth_case', 'false', |
| 204 | | """Whether case should be ignored for login names (''since 0.9'').""") |
| 205 | | |
| 206 | | auth_schema = Option('account-manager', 'auth_schema', '', |
| 207 | | """PostgreSQL schema in which the auth_cookie table resides.""") |
| 208 | | |
| 209 | | # IAuthenticator methods |
| 210 | | |
| 211 | | def authenticate(self, req): |
| 212 | | if req.method == 'POST' and req.path_info.startswith('/login'): |
| 213 | | req.environ['REMOTE_USER'] = self._remote_user(req) |
| 214 | | authname = None |
| 215 | | if req.remote_user: |
| 216 | | authname = req.remote_user |
| 217 | | elif req.incookie.has_key('trac_auth'): |
| 218 | | authname = self._get_name_for_cookie(req, req.incookie['trac_auth']) |
| 219 | | |
| 220 | | if not authname: |
| 221 | | return None |
| 222 | | |
| 223 | | if self.ignore_case: |
| 224 | | authname = authname.lower() |
| 225 | | |
| 226 | | return authname |
| 227 | | authenticate = if_enabled(authenticate) |
| 228 | | |
| 229 | | # INavigationContributor methods |
| 230 | | |
| 231 | | def get_active_navigation_item(self, req): |
| 232 | | return 'login' |
| 233 | | |
| 234 | | def get_navigation_items(self, req): |
| 235 | | if req.authname and req.authname != 'anonymous': |
| 236 | | yield ('metanav', 'login', 'logged in as %s' % req.authname) |
| 237 | | yield ('metanav', 'logout', |
| 238 | | Markup('<a href="%s">Logout</a>' |
| 239 | | % escape(self.env.href.logout()))) |
| 240 | | else: |
| 241 | | yield ('metanav', 'login', |
| 242 | | Markup('<a href="%s">Login</a>' |
| 243 | | % escape(self.env.href.login()))) |
| 244 | | |
| 245 | | # IRequestHandler methods |
| 246 | | |
| 247 | | def match_request(self, req): |
| 248 | | return re.match('/(login|logout)/?', req.path_info) |
| 249 | | match_request = if_enabled(match_request) |
| 250 | | |
| 251 | | def process_request(self, req): |
| 252 | | self.log.debug('path_info: %s' % str(req.path_info)) |
| 253 | | self.log.debug('authname: %s' % str(req.authname)) |
| 254 | | if req.path_info.startswith('/login') and req.authname == 'anonymous': |
| 255 | | req.hdf['referer'] = self._referer(req) |
| 256 | | if req.method == 'POST': |
| 257 | | req.hdf['login.error'] = 'Invalid username or password' |
| 258 | | return 'login.cs', None |
| 259 | | if req.path_info.startswith('/login'): |
| 260 | | self._do_login(req) |
| 261 | | elif req.path_info.startswith('/logout'): |
| 262 | | self._do_logout(req) |
| 263 | | self._redirect_back(req) |
| 264 | | |
| 265 | | # Protect against module contention |
| 266 | | def enabled(self): |
| 267 | | # Users should disable the built-in authentication to use this one |
| 268 | | return not self.env.is_component_enabled(auth.LoginModule) |
| 269 | | enabled = property(enabled) |
| 270 | | |
| 271 | | # Internal methods |
| 272 | | |
| 273 | | def _remote_user(self, req): |
| 274 | | user = req.args.get('user') |
| 275 | | if AccountManager(self.env).check_password(user, |
| 276 | | req.args.get('password')): |
| 277 | | return user |
| 278 | | return None |
| 279 | | |
| 280 | | def _do_login(self, req): |
| 281 | | if not req.remote_user: |
| 282 | | req.redirect(self.env.abs_href()) |
| 283 | | assert req.remote_user, 'Authentication information not available.' |
| 284 | | |
| 285 | | remote_user = req.remote_user |
| 286 | | if self.ignore_case: |
| 287 | | remote_user = remote_user.lower() |
| 288 | | |
| 289 | | assert req.authname in ('anonymous', remote_user), \ |
| 290 | | 'Already logged in as %s.' % req.authname |
| 291 | | |
| 292 | | cookie = hex_entropy() |
| 293 | | db = self.env.get_db_cnx() |
| 294 | | cursor = db.cursor() |
| 295 | | # auth_schema = self.env.config.get('acct_mgr', 'auth_schema', db.schema) |
| 296 | | sql = "INSERT INTO %s.auth_cookie (cookie,name,ipnr,time)" % \ |
| 297 | | self.auth_schema or db.schema |
| 298 | | cursor.execute(sql + " VALUES (%s, %s, %s, %s)", (cookie, remote_user, |
| 299 | | req.remote_addr, int(time.time()))) |
| 300 | | db.commit() |
| 301 | | |
| 302 | | req.authname = remote_user |
| 303 | | req.outcookie['trac_auth'] = cookie |
| 304 | | req.outcookie['trac_auth']['path'] = '/' |
| 305 | | domain = self.env.config.get('acct_mgr', 'password_domain', 'pacopablo.com') |
| 306 | | req.outcookie['trac_auth']['domain'] = ''.join(['.', domain]) |
| 307 | | |
| 308 | | def _do_logout(self, req): |
| 309 | | """Log the user out. |
| 310 | | |
| 311 | | Simply deletes the corresponding record from the auth_cookie table. |
| 312 | | """ |
| 313 | | if req.authname == 'anonymous': |
| 314 | | # Not logged in |
| 315 | | return |
| 316 | | |
| 317 | | # While deleting this cookie we also take the opportunity to delete |
| 318 | | # cookies older than 10 days |
| 319 | | db = self.env.get_db_cnx() |
| 320 | | cursor = db.cursor() |
| 321 | | # auth_schema = self.env.config.get('acct_mgr', 'auth_schema', db.schema) |
| 322 | | sql = "DELETE FROM %s.auth_cookie " % self.auth_schema or db.schema |
| 323 | | cursor.execute(sql + " WHERE name=%s OR time < %s", |
| 324 | | (req.authname, int(time.time()) - 86400 * 10)) |
| 325 | | db.commit() |
| 326 | | self._expire_cookie(req) |
| 327 | | |
| 328 | | def _expire_cookie(self, req): |
| 329 | | """Instruct the user agent to drop the auth cookie by setting the |
| 330 | | "expires" property to a date in the past. |
| 331 | | """ |
| 332 | | req.outcookie['trac_auth'] = '' |
| 333 | | req.outcookie['trac_auth']['path'] = self.env.href() |
| 334 | | req.outcookie['trac_auth']['expires'] = -10000 |
| 335 | | |
| 336 | | def _get_name_for_cookie(self, req, cookie): |
| 337 | | db = self.env.get_db_cnx() |
| 338 | | cursor = db.cursor() |
| 339 | | # auth_schema = self.env.config.get('acct_mgr', 'auth_schema', db.schema) |
| 340 | | sql = "SELECT name FROM %s.auth_cookie " % self.auth_schema or db.schema |
| 341 | | if self.check_ip: |
| 342 | | self.log.debug('cooke.value: %s' % str(cookie.value)) |
| 343 | | self.log.debug('req.remote_addr: %s' % str(req.remote_addr)) |
| 344 | | self.log.debug('sql: %s' % str(sql)) |
| 345 | | cursor.execute(sql + " WHERE cookie=%s AND ipnr=%s", |
| 346 | | (cookie.value, req.remote_addr)) |
| 347 | | else: |
| 348 | | cursor.execute(sql + " WHERE cookie=%s", (cookie.value,)) |
| 349 | | row = cursor.fetchone() |
| 350 | | if not row: |
| 351 | | # The cookie is invalid (or has been purged from the database), so |
| 352 | | # tell the user agent to drop it as it is invalid |
| 353 | | self._expire_cookie(req) |
| 354 | | return None |
| 355 | | |
| 356 | | return row[0] |
| 357 | | |
| 358 | | def _redirect_back(self, req): |
| 359 | | """Redirect the user back to the URL she came from.""" |
| 360 | | referer = self._referer(req) |
| 361 | | if referer and not referer.startswith(req.base_url): |
| 362 | | # don't redirect to external sites |
| 363 | | referer = None |
| 364 | | req.redirect(referer or self.env.abs_href()) |
| 365 | | |
| 366 | | def _referer(self, req): |
| 367 | | return req.args.get('referer') or req.get_header('Referer') |
| 368 | | |
| 369 | | |
| 370 | | |
| 371 | | |
| 372 | | |