Split the admin cookie out from the session cookie.

This commit is contained in:
Neil Williams
2012-03-12 15:32:56 -07:00
parent e186b4cd98
commit 33b15bc265
8 changed files with 82 additions and 33 deletions

View File

@@ -91,6 +91,8 @@ shutdown_secret = 12345
https_endpoint =
# name of the cookie to drop with login information
login_cookie = reddit_session
# name of the admin cookie
admin_cookie = reddit_admin
# the work factor for bcrypt, increment this every time computers double in
# speed. don't worry, changing this won't break old passwords
bcrypt_work_factor = 12
@@ -385,6 +387,8 @@ REPLY_AGE_LIMIT = 180
VOTE_AGE_LIMIT = 180
# minimum age, in days, of an account to be eligible to create a community
min_membership_create_community = 30
# how long the admin cookie should be valid for (in seconds)
ADMIN_COOKIE_TTL = 1800
# min amount of karma to edit
WIKI_KARMA = 100

View File

@@ -2462,5 +2462,5 @@ class ApiController(RedditController):
if form.has_errors('password', errors.WRONG_PASSWORD):
return
self.login(c.user, admin = True, rem = True)
self.enable_admin_mode(c.user)
form.redirect(dest)

View File

@@ -36,10 +36,9 @@ class ButtonjsController(MinimalController):
MinimalController.pre(self)
# override user loggedin behavior to ensure this page always
# uses the page cache
(user, maybe_admin) = \
valid_cookie(c.cookies[g.login_cookie].value
if g.login_cookie in c.cookies
else '')
user = valid_cookie(c.cookies[g.login_cookie].value
if g.login_cookie in c.cookies
else '')
if user:
self.user_is_loggedin = True

View File

@@ -1076,7 +1076,7 @@ class FormsController(RedditController):
"""disable admin interaction with site."""
if not c.user.name in g.admins:
return self.abort404()
self.login(c.user, admin = False, rem = True)
self.disable_admin_mode(c.user)
return self.redirect(dest)
def GET_validuser(self):

View File

@@ -29,7 +29,7 @@ from r2.lib import pages, utils, filters, amqp, stats
from r2.lib.utils import http_utils, is_subdomain, UniqueIterator, ip_and_slash16
from r2.lib.cache import LocalCache, make_key, MemcachedError
import random as rand
from r2.models.account import valid_cookie, FakeAccount, valid_feed
from r2.models.account import valid_cookie, FakeAccount, valid_feed, valid_admin_cookie
from r2.models.subreddit import Subreddit
from r2.models import *
from errors import ErrorSet
@@ -40,7 +40,7 @@ from r2.lib.jsontemplates import api_type, is_api
from Cookie import CookieError
from copy import copy
from Cookie import CookieError
from datetime import datetime
from datetime import datetime, timedelta
from hashlib import sha1, md5
from urllib import quote, unquote
import simplejson
@@ -746,14 +746,25 @@ class MinimalController(BaseController):
class RedditController(MinimalController):
@staticmethod
def login(user, admin = False, rem = False):
c.cookies[g.login_cookie] = Cookie(value = user.make_cookie(admin = admin),
def login(user, rem=False):
c.cookies[g.login_cookie] = Cookie(value = user.make_cookie(),
expires = NEVER if rem else None)
@staticmethod
def logout(admin = False):
def logout():
c.cookies[g.login_cookie] = Cookie(value='', expires=DELETE)
@staticmethod
def enable_admin_mode(user):
expiration_time = datetime.utcnow() + timedelta(seconds=g.ADMIN_COOKIE_TTL)
expiration = expiration_time.strftime('%a, %d %b %Y %H:%M:%S GMT')
c.cookies[g.admin_cookie] = Cookie(value=user.make_admin_cookie(),
expires=expiration)
@staticmethod
def disable_admin_mode(user):
c.cookies[g.admin_cookie] = Cookie(value='', expires=DELETE)
def pre(self):
c.response_wrappers = []
MinimalController.pre(self)
@@ -781,12 +792,15 @@ class RedditController(MinimalController):
# no logins for RSS feed unless valid_feed has already been called
if not c.user:
if c.extension != "rss":
(c.user, maybe_admin) = \
valid_cookie(c.cookies[g.login_cookie].value
if g.login_cookie in c.cookies
else '')
if c.user:
c.user_is_loggedin = True
session_cookie = c.cookies.get(g.login_cookie)
if session_cookie:
c.user = valid_cookie(session_cookie.value)
if c.user:
c.user_is_loggedin = True
admin_cookie = c.cookies.get(g.admin_cookie)
if c.user_is_loggedin and admin_cookie:
maybe_admin = valid_admin_cookie(admin_cookie.value)
if not c.user:
c.user = UnloggedUser(get_browser_langs())

View File

@@ -116,6 +116,7 @@ class Globals(object):
'MODWINDOW',
'RATELIMIT',
'QUOTA_THRESHOLD',
'ADMIN_COOKIE_TTL',
'num_comments',
'max_comments',
'max_comments_gold',

View File

@@ -753,11 +753,10 @@ class AdminModeInterstitial(BoringPage):
*args, **kwargs)
def content(self):
return PasswordVerificationForm("adminon", dest=self.dest)
return PasswordVerificationForm(dest=self.dest)
class PasswordVerificationForm(Templated):
def __init__(self, api, dest):
self.api = api
def __init__(self, dest):
self.dest = dest
Templated.__init__(self)

View File

@@ -30,12 +30,15 @@ from r2.lib.cache import sgm
from r2.lib import filters
from r2.lib.log import log_text
from pylons import g
from pylons import c, g, request
from pylons.i18n import _
import time, sha
from copy import copy
from datetime import datetime, timedelta
import bcrypt
import hmac
import hashlib
class AccountExists(Exception): pass
@@ -202,16 +205,22 @@ class Account(Thing):
(self.name, prev_visit, current_time))
set_last_visit(self)
def make_cookie(self, timestr = None, admin = False):
def make_cookie(self, timestr=None):
if not self._loaded:
self._load()
timestr = timestr or time.strftime('%Y-%m-%dT%H:%M:%S')
id_time = str(self._id) + ',' + timestr
to_hash = ','.join((id_time, self.password, g.SECRET))
if admin:
to_hash += 'admin'
return id_time + ',' + sha.new(to_hash).hexdigest()
def make_admin_cookie(self, timestr=None):
if not self._loaded:
self._load()
timestr = timestr or datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S')
hashable = ','.join((timestr, request.ip, request.user_agent, self.password))
mac = hmac.new(g.SECRET, hashable, hashlib.sha1).hexdigest()
return ','.join((timestr, mac))
def needs_captcha(self):
return not g.disable_captcha and self.link_karma < 1
@@ -554,23 +563,46 @@ def valid_cookie(cookie):
uid, timestr, hash = cookie.split(',')
uid = int(uid)
except:
return (False, False)
return False
if g.read_only_mode:
return (False, False)
return False
try:
account = Account._byID(uid, True)
if account._deleted:
return (False, False)
return False
except NotFound:
return (False, False)
return False
if constant_time_compare(cookie, account.make_cookie(timestr)):
return account
return False
def valid_admin_cookie(cookie):
if g.read_only_mode:
return False
# parse the cookie
try:
timestr, hash = cookie.split(',')
except ValueError:
return False
# make sure it's a recent cookie
try:
cookie_time = datetime.strptime(timestr, '%Y-%m-%dT%H:%M:%S')
except ValueError:
return False
cookie_age = datetime.utcnow() - cookie_time
if cookie_age.total_seconds() > g.ADMIN_COOKIE_TTL:
return False
# validate
return constant_time_compare(cookie, c.user.make_admin_cookie(timestr))
if constant_time_compare(cookie, account.make_cookie(timestr, admin = False)):
return (account, False)
elif constant_time_compare(cookie, account.make_cookie(timestr, admin = True)):
return (account, True)
return (False, False)
def valid_feed(name, feedhash, path):
if name and feedhash and path: