Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Commit

Permalink
Split the admin cookie out from the session cookie.
Browse files Browse the repository at this point in the history
  • Loading branch information
spladug committed Mar 13, 2012
1 parent e186b4c commit 33b15bc
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 33 deletions.
4 changes: 4 additions & 0 deletions r2/example.ini
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion r2/r2/controllers/api.py
Expand Up @@ -2462,5 +2462,5 @@ def POST_adminon(self, form, jquery, dest):
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)
7 changes: 3 additions & 4 deletions r2/r2/controllers/buttons.py
Expand Up @@ -36,10 +36,9 @@ def pre(self):
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

Expand Down
2 changes: 1 addition & 1 deletion r2/r2/controllers/front.py
Expand Up @@ -1076,7 +1076,7 @@ def GET_adminoff(self, dest):
"""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):
Expand Down
36 changes: 25 additions & 11 deletions r2/r2/controllers/reddit_base.py
Expand Up @@ -29,7 +29,7 @@
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
Expand All @@ -40,7 +40,7 @@
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
Expand Down Expand Up @@ -746,14 +746,25 @@ def api_wrapper(self, kw):
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)
Expand Down Expand Up @@ -781,12 +792,15 @@ def pre(self):
# 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())
Expand Down
1 change: 1 addition & 0 deletions r2/r2/lib/app_globals.py
Expand Up @@ -116,6 +116,7 @@ class Globals(object):
'MODWINDOW',
'RATELIMIT',
'QUOTA_THRESHOLD',
'ADMIN_COOKIE_TTL',
'num_comments',
'max_comments',
'max_comments_gold',
Expand Down
5 changes: 2 additions & 3 deletions r2/r2/lib/pages/pages.py
Expand Up @@ -753,11 +753,10 @@ def __init__(self, dest, *args, **kwargs):
*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)

Expand Down
58 changes: 45 additions & 13 deletions r2/r2/models/account.py
Expand Up @@ -30,12 +30,15 @@
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

Expand Down Expand Up @@ -202,16 +205,22 @@ def update_last_visit(self, current_time):
(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

Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 33b15bc

Please sign in to comment.