Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 1050 lines (853 sloc) 35.719 kb
df971c3 fix the licenses so they stop polluting all the other diffs
spez authored
1 # The contents of this file are subject to the Common Public Attribution
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
2 # License Version 1.0. (the "License"); you may not use this file except in
3 # compliance with the License. You may obtain a copy of the License at
4 # http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
5 # License Version 1.1, but Sections 14 and 15 have been added to cover use of
6 # software over a computer network and provide for limited attribution for the
7 # Original Developer. In addition, Exhibit A has been modified to be consistent
8 # with Exhibit B.
2869eaf @ketralnis New features:
ketralnis authored
9 #
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
10 # Software distributed under the License is distributed on an "AS IS" basis,
11 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
12 # the specific language governing rights and limitations under the License.
2869eaf @ketralnis New features:
ketralnis authored
13 #
914b949 @spladug Update / add license headers.
spladug authored
14 # The Original Code is reddit.
2869eaf @ketralnis New features:
ketralnis authored
15 #
914b949 @spladug Update / add license headers.
spladug authored
16 # The Original Developer is the Initial Developer. The Initial Developer of
17 # the Original Code is reddit Inc.
2869eaf @ketralnis New features:
ketralnis authored
18 #
af09fa8 @spladug Update license headers to 2015.
spladug authored
19 # All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
914b949 @spladug Update / add license headers.
spladug authored
20 # Inc. All Rights Reserved.
21 ###############################################################################
22
25a890d @JordanMilne Replace `disable_require_employee_https` with a feature flag
JordanMilne authored
23 from r2.config import feature
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
24 from r2.lib.db.thing import Thing, Relation, NotFound
25 from r2.lib.db.operators import lower
26 from r2.lib.db.userrel import UserRel
2fe83ed @alienth Add AccountActivityBySR.
alienth authored
27 from r2.lib.db import tdb_cassandra
e35b519 * removed references to clear_memo
spez authored
28 from r2.lib.memoize import memoize
8a5f685 @chromakode Move private-shadowed utils into separate module.
chromakode authored
29 from r2.lib.admin_utils import modhash, valid_hash
30 from r2.lib.utils import randstr, timefromnow
2243af7 @spladug Clean out dead last-modified rel code.
spladug authored
31 from r2.lib.utils import UrlParser
76594cd @spladug Refactor and reorganize email canonicalization; add some tests.
spladug authored
32 from r2.lib.utils import constant_time_compare, canonicalize_email
0bceab8 @JordanMilne Add queue to scrub deleted accounts of potential leaks
JordanMilne authored
33 from r2.lib import amqp, filters, hooks
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
34 from r2.lib.log import log_text
555c02a @spladug Dual-write last visit timestamp.
spladug authored
35 from r2.models.last_modified import LastModified
b24ba98 @Deimos Refactor flair API endpoints
Deimos authored
36 from r2.models.modaction import ModAction
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
37 from r2.models.trylater import TryLater
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
38
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
39 from pylons import c, g, request
88d191e @spladug Gold feature: personal karma breakdown by subreddit.
spladug authored
40 from pylons.i18n import _
c9c65dd @spladug Replace references to deprecated sha module with hashlib.
spladug authored
41 import time
42 import hashlib
b8d9981 @Deimos Refactor ProfileBar and Account.all_karmas
Deimos authored
43 from collections import Counter, OrderedDict
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
44 from copy import copy
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
45 from datetime import datetime, timedelta
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
46 import bcrypt
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
47 import hmac
48 import hashlib
3437713 @atiaxi Email: Use non-hardcache ban fetching system
atiaxi authored
49 import itertools
2fe83ed @alienth Add AccountActivityBySR.
alienth authored
50 from pycassa.system_manager import ASCII_TYPE
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
51
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
52
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
53 trylater_hooks = hooks.HookRegistrar()
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
54 COOKIE_TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S'
55
56
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
57 class AccountExists(Exception): pass
58
59 class Account(Thing):
60 _data_int_props = Thing._data_int_props + ('link_karma', 'comment_karma',
61 'report_made', 'report_correct',
62 'report_ignored', 'spammer',
09c98d6 @umbrae Inbox counts: Add dark unread counts badge, start writing to inbox_count
umbrae authored
63 'reported', 'gold_creddits',
64 'inbox_count',
b76322f @bsimpson63 Add alerts for suspicious selfserve advertising payments.
bsimpson63 authored
65 'num_payment_methods',
66 'num_failed_payments',
09c98d6 @umbrae Inbox counts: Add dark unread counts badge, start writing to inbox_count
umbrae authored
67 )
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
68 _int_prop_suffix = '_karma'
9a4271f @KeyserSosa Upgrade Instructions
KeyserSosa authored
69 _essentials = ('name', )
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
70 _defaults = dict(pref_numsites = 25,
71 pref_frame = False,
e7eb9e6 comments panel off by default
spez authored
72 pref_frame_commentspanel = False,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
73 pref_newwindow = False,
ae9eaf5 add preference to remove recently clicked widget
spez authored
74 pref_clickgadget = 5,
ff002a4 @ketralnis Add backend for new gold feature: "remember my visits".
ketralnis authored
75 pref_store_visits = False,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
76 pref_public_votes = False,
1b030c3 @raugturi Added option to hide user profile from robots.
raugturi authored
77 pref_hide_from_robots = False,
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
78 pref_research = False,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
79 pref_hide_ups = False,
4440ccf @KeyserSosa overhaul of JS and form handling code, not based on jQuery
KeyserSosa authored
80 pref_hide_downs = False,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
81 pref_min_link_score = -4,
82 pref_min_comment_score = -4,
83 pref_num_comments = g.num_comments,
c47c697 @umbrae Add optional controversial indicator to comment scores
umbrae authored
84 pref_highlight_controversial=False,
52d419f @xiongchiamiov Comment sort preference: dual-write to migrate
xiongchiamiov authored
85 pref_default_comment_sort = None,
0745f5b @KeyserSosa make 1/2 of the default reddits English reddits if the user hasn't set t...
KeyserSosa authored
86 pref_lang = g.lang,
87 pref_content_langs = (g.lang,),
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
88 pref_over_18 = False,
89 pref_compress = False,
7e6cbc4 @Deimos Domain area: add new user pref to show extra info
Deimos authored
90 pref_domain_details = False,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
91 pref_organic = True,
08c431b @KeyserSosa * Comply with the spec on 304 errors so Chrome won't barf download.gz a...
KeyserSosa authored
92 pref_no_profanity = True,
5ef76b9 @KeyserSosa New features:
KeyserSosa authored
93 pref_label_nsfw = True,
6bcef00 @ketralnis 1. Allow a reddit to have a cname, like www.proggit.com, that renders
ketralnis authored
94 pref_show_stylesheets = True,
4194ced Add a user preference for whether to show flair.
Logan Hanks authored
95 pref_show_flair = True,
13e191b Add a user preference for showing link flair.
Logan Hanks authored
96 pref_show_link_flair = True,
5ef76b9 @KeyserSosa New features:
KeyserSosa authored
97 pref_mark_messages_read = True,
2869eaf @ketralnis New features:
ketralnis authored
98 pref_threaded_messages = True,
99 pref_collapse_read_messages = False,
9c6aa71 @xiongchiamiov Add option for sending orangereds as emails
xiongchiamiov authored
100 pref_email_messages = False,
a402d48 New features:
Mike authored
101 pref_private_feeds = True,
b70556a @JordanMilne Add support for forced HTTPS with HSTS grants
JordanMilne authored
102 pref_force_https = False,
0ae8f2f @ketralnis 21 Jul 2010 merge
ketralnis authored
103 pref_show_adbox = True,
7fff900 @ketralnis February 2011 Merge
ketralnis authored
104 pref_show_sponsors = True, # sponsored links
105 pref_show_sponsorships = True,
a453758 @umbrae Add a preference for trending on the front page
umbrae authored
106 pref_show_trending=True,
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
107 pref_highlight_new_comments = True,
e057278 @spladug Gold Feature: "The Butler". Username monitoring in comments.
spladug authored
108 pref_monitor_mentions=True,
07366ac Move left bar collapsing to server-side preference.
Max Goodman authored
109 pref_collapse_left_bar=False,
8c9b31d @bsimpson63 ServerSecondsBar can be made public.
bsimpson63 authored
110 pref_public_server_seconds=False,
9a4271f @KeyserSosa Upgrade Instructions
KeyserSosa authored
111 mobile_compress = False,
112 mobile_thumbnail = True,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
113 reported = 0,
114 report_made = 0,
115 report_correct = 0,
116 report_ignored = 0,
117 spammer = 0,
118 sort_options = {},
950971c added last modified to profile/comment pages
shuffman authored
119 has_subscribed = False,
0c04094 @KeyserSosa updates to RSS feed to link to permalink page instead of goto. Also add...
KeyserSosa authored
120 pref_media = 'subreddit',
603ec0e @rram wiki: Fix bug with wiki permissions.
rram authored
121 wiki_override = None,
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
122 email = "",
e87f520 @KeyserSosa New Features:
KeyserSosa authored
123 email_verified = False,
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
124 ignorereports = False,
0ae8f2f @ketralnis 21 Jul 2010 merge
ketralnis authored
125 pref_show_promote = None,
126 gold = False,
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
127 gold_charter = False,
7fff900 @ketralnis February 2011 Merge
ketralnis authored
128 gold_creddits = 0,
acd485e @spladug Add default for new cake system.
spladug authored
129 cake_expiration=None,
8dfd73b @spladug Add framework for RFC-6238: Time-Based One Time Password Algorithm.
spladug authored
130 otp_secret=None,
65b1467 @atiaxi Backend support for bans.
atiaxi authored
131 state=0,
5e249f4 @spladug Make all moderators have a modmsgtime attribute.
spladug authored
132 modmsgtime=None,
09c98d6 @umbrae Inbox counts: Add dark unread counts badge, start writing to inbox_count
umbrae authored
133 inbox_count=0,
594b6bf @atiaxi Listingcontroller: Optionally display banned userpages
atiaxi authored
134 banned_profile_visible=False,
ac83646 @bsimpson63 Add a notice when localized default subreddits are used.
bsimpson63 authored
135 pref_use_global_defaults=False,
136 pref_hide_locationbar=False,
82b870d @Deimos Gold: add preference to auto-renew with a creddit
Deimos authored
137 pref_creddit_autorenew=False,
f963cb7 @Deimos Support certain accounts never adding to sent
Deimos authored
138 update_sent_messages=True,
0055f9a @MelissaCole Can reveal identity or write a message in gildings
MelissaCole authored
139 num_payment_methods=0,
140 num_failed_payments=0,
141 pref_show_snoovatar=False,
142 gild_reveal_username=False,
dbb7935 @bsimpson63 Enable per-user global selfserve cpm override.
bsimpson63 authored
143 selfserve_cpm_override_pennies=None,
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
144 )
c413a2e @kemitche Add PATCH /api/v1/me/prefs endpoint
kemitche authored
145 _preference_attrs = tuple(k for k in _defaults.keys()
146 if k.startswith("pref_"))
147
148 def preferences(self):
149 return {pref: getattr(self, pref) for pref in self._preference_attrs}
950971c added last modified to profile/comment pages
shuffman authored
150
b06dc9b Add equality implementation for Account model.
Max Goodman authored
151 def __eq__(self, other):
152 if type(self) != type(other):
153 return False
154
155 return self._id == other._id
156
157 def __ne__(self, other):
158 return not self.__eq__(other)
159
e8e751a @spladug Don't spam ban messages for users who don't care.
spladug authored
160 def has_interacted_with(self, sr):
161 if not sr:
162 return False
163
164 for type in ('link', 'comment'):
165 if hasattr(self, "%s_%s_karma" % (sr.name, type)):
166 return True
167
168 if sr.is_subscriber(self):
169 return True
170
171 return False
172
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
173 def karma(self, kind, sr = None):
174 suffix = '_' + kind + '_karma'
e87f520 @KeyserSosa New Features:
KeyserSosa authored
175
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
176 #if no sr, return the sum
177 if sr is None:
178 total = 0
179 for k, v in self._t.iteritems():
180 if k.endswith(suffix):
181 total += v
182 return total
183 else:
184 try:
185 return getattr(self, sr.name + suffix)
186 except AttributeError:
187 #if positive karma elsewhere, you get min_up_karma
188 if self.karma(kind) > 0:
189 return g.MIN_UP_KARMA
190 else:
191 return 0
192
193 def incr_karma(self, kind, sr, amt):
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
194 if sr.name.startswith('_'):
195 g.log.info("Ignoring karma increase for subreddit %r" % (sr.name,))
196 return
197
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
198 prop = '%s_%s_karma' % (sr.name, kind)
199 if hasattr(self, prop):
200 return self._incr(prop, amt)
201 else:
202 default_val = self.karma(kind, sr)
203 setattr(self, prop, default_val + amt)
204 self._commit()
205
206 @property
207 def link_karma(self):
208 return self.karma('link')
209
210 @property
211 def comment_karma(self):
212 return self.karma('comment')
213
a635c9c @andre-d karma: Add karma list api endpoint.
andre-d authored
214 def all_karmas(self, include_old=True):
b8d9981 @Deimos Refactor ProfileBar and Account.all_karmas
Deimos authored
215 """Get all of the user's subreddit-specific karma totals.
216
217 Returns an OrderedDict keyed on subreddit name and containing
218 (link_karma, comment_karma) tuples, ordered by the combined total
219 descending.
220 """
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
221 link_suffix = '_link_karma'
222 comment_suffix = '_comment_karma'
b8d9981 @Deimos Refactor ProfileBar and Account.all_karmas
Deimos authored
223
224 comment_karmas = Counter()
225 link_karmas = Counter()
226 combined_karmas = Counter()
227
228 for key, value in self._t.iteritems():
229 if key.endswith(link_suffix):
230 sr_name = key[:-len(link_suffix)]
231 link_karmas[sr_name] = value
232 elif key.endswith(comment_suffix):
233 sr_name = key[:-len(comment_suffix)]
234 comment_karmas[sr_name] = value
235 else:
236 continue
237
238 combined_karmas[sr_name] += value
239
240 all_karmas = OrderedDict()
241 for sr_name, total in combined_karmas.most_common():
242 all_karmas[sr_name] = (link_karmas[sr_name],
243 comment_karmas[sr_name])
244
245 if include_old:
246 old_link_karma = self._t.get('link_karma', 0)
247 old_comment_karma = self._t.get('comment_karma', 0)
248 if old_link_karma or old_comment_karma:
249 all_karmas['ancient history'] = (old_link_karma,
250 old_comment_karma)
251
252 return all_karmas
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
253
254 def update_last_visit(self, current_time):
255 from admintools import apply_updates
256
257 apply_updates(self)
258
14cb34c @spladug LastModified: cut reads for Last Visit over to new schema.
spladug authored
259 prev_visit = LastModified.get(self._fullname, "Visit")
260 if prev_visit and current_time - prev_visit < timedelta(days=1):
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
261 return
262
9a4271f @KeyserSosa Upgrade Instructions
KeyserSosa authored
263 g.log.debug ("Updating last visit for %s from %s to %s" %
264 (self.name, prev_visit, current_time))
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
265
555c02a @spladug Dual-write last visit timestamp.
spladug authored
266 LastModified.touch(self._fullname, "Visit")
267
b71d8bf @alienth Store a last_visit time on the Account thing.
alienth authored
268 self.last_visit = int(time.time())
269 self._commit()
270
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
271 def make_cookie(self, timestr=None):
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
272 if not self._loaded:
273 self._load()
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
274 timestr = timestr or time.strftime(COOKIE_TIMESTAMP_FORMAT)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
275 id_time = str(self._id) + ',' + timestr
3366083 @spladug Create a vault for secret tokens and move some into it.
spladug authored
276 to_hash = ','.join((id_time, self.password, g.secrets["SECRET"]))
c9c65dd @spladug Replace references to deprecated sha module with hashlib.
spladug authored
277 return id_time + ',' + hashlib.sha1(to_hash).hexdigest()
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
278
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
279 def make_admin_cookie(self, first_login=None, last_request=None):
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
280 if not self._loaded:
281 self._load()
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
282 first_login = first_login or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)
283 last_request = last_request or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)
284 hashable = ','.join((first_login, last_request, request.ip, request.user_agent, self.password))
3366083 @spladug Create a vault for secret tokens and move some into it.
spladug authored
285 mac = hmac.new(g.secrets["SECRET"], hashable, hashlib.sha1).hexdigest()
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
286 return ','.join((first_login, last_request, mac))
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
287
8dfd73b @spladug Add framework for RFC-6238: Time-Based One Time Password Algorithm.
spladug authored
288 def make_otp_cookie(self, timestamp=None):
289 if not self._loaded:
290 self._load()
291
292 timestamp = timestamp or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)
293 secrets = [request.user_agent, self.otp_secret, self.password]
3366083 @spladug Create a vault for secret tokens and move some into it.
spladug authored
294 signature = hmac.new(g.secrets["SECRET"], ','.join([timestamp] + secrets), hashlib.sha1).hexdigest()
8dfd73b @spladug Add framework for RFC-6238: Time-Based One Time Password Algorithm.
spladug authored
295
296 return ",".join((timestamp, signature))
297
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
298 def needs_captcha(self):
9c8be78 @Deimos Account.needs_captcha: add hook, use live_config
Deimos authored
299 if g.disable_captcha:
300 return False
301
302 hook = hooks.get_hook("account.is_captcha_exempt")
303 captcha_exempt = hook.call_until_return(account=self)
304 if captcha_exempt:
305 return False
306
307 if self.link_karma >= g.live_config["captcha_exempt_link_karma"]:
308 return False
309
310 if self.comment_karma >= g.live_config["captcha_exempt_comment_karma"]:
311 return False
312
313 return True
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
314
165ca78 @Deimos Create subreddit: add support for min age/karma
Deimos authored
315 @property
316 def can_create_subreddit(self):
317 hook = hooks.get_hook("account.can_create_subreddit")
318 can_create = hook.call_until_return(account=self)
319 if can_create is not None:
320 return can_create
321
322 min_age = timedelta(days=g.live_config["create_sr_account_age_days"])
323 if self._age < min_age:
324 return False
325
326 if (self.link_karma < g.live_config["create_sr_link_karma"] and
327 self.comment_karma < g.live_config["create_sr_comment_karma"]):
328 return False
329
330 return True
331
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
332 def modhash(self, rand=None, test=False):
d6848c8 @kemitche [OAuth2] Don't send unnecessary modhash to OAuth clients
kemitche authored
333 if c.oauth_user:
334 # OAuth clients should never receive a modhash of any kind
335 # as they could use it in a CSRF attack to bypass their
336 # permitted OAuth scopes.
337 return None
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
338 return modhash(self, rand = rand, test = test)
339
340 def valid_hash(self, hash):
deff940 Add OAuth2 handling to the main APIController.
Max Goodman authored
341 if self == c.oauth_user:
342 # OAuth authenticated requests do not require CSRF protection.
343 return True
344 else:
345 return valid_hash(self, hash)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
346
347 @classmethod
348 @memoize('account._by_name')
349 def _by_name_cache(cls, name, allow_deleted = False):
350 #relower name here, just in case
351 deleted = (True, False) if allow_deleted else False
352 q = cls._query(lower(Account.c.name) == name.lower(),
353 Account.c._spam == (True, False),
354 Account.c._deleted == deleted)
355
356 q._limit = 1
357 l = list(q)
358 if l:
359 return l[0]._id
360
361 @classmethod
e35b519 * removed references to clear_memo
spez authored
362 def _by_name(cls, name, allow_deleted = False, _update = False):
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
363 #lower name here so there is only one cache
e35b519 * removed references to clear_memo
spez authored
364 uid = cls._by_name_cache(name.lower(), allow_deleted, _update = _update)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
365 if uid:
366 return cls._byID(uid, True)
367 else:
368 raise NotFound, 'Account %s' % name
369
ee5ea8c @umbrae Inbox_counts: corrections on deletes, spams, edits
umbrae authored
370 @classmethod
371 def _names_to_ids(cls, names, ignore_missing=False, allow_deleted=False,
372 _update=False):
373 for name in names:
374 uid = cls._by_name_cache(name.lower(), allow_deleted, _update=_update)
375 if not uid:
376 if ignore_missing:
377 continue
378 raise NotFound('Account %s' % name)
379 yield uid
380
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
381 # Admins only, since it's not memoized
382 @classmethod
383 def _by_name_multiple(cls, name):
384 q = cls._query(lower(Account.c.name) == name.lower(),
385 Account.c._spam == (True, False),
386 Account.c._deleted == (True, False))
387 return list(q)
388
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
389 @property
390 def friends(self):
391 return self.friend_ids()
392
4d7a2fa @kemitche Allow users to block users that harass them
kemitche authored
393 @property
394 def enemies(self):
395 return self.enemy_ids()
396
5e249f4 @spladug Make all moderators have a modmsgtime attribute.
spladug authored
397 @property
398 def is_moderator_somewhere(self):
399 # modmsgtime can be:
400 # - a date: the user is a mod somewhere and has unread modmail
401 # - False: the user is a mod somewhere and has no unread modmail
402 # - None: (the default) the user is not a mod anywhere
403 return self.modmsgtime is not None
404
0ae8f2f @ketralnis 21 Jul 2010 merge
ketralnis authored
405 # Used on the goldmember version of /prefs/friends
406 @memoize('account.friend_rels')
407 def friend_rels_cache(self):
408 q = Friend._query(Friend.c._thing1_id == self._id,
409 Friend.c._name == 'friend')
410 return list(f._id for f in q)
411
412 def friend_rels(self, _update = False):
413 rel_ids = self.friend_rels_cache(_update=_update)
37e2ba9 @ketralnis * Combine cassandra clusters into a single one
ketralnis authored
414 try:
415 rels = Friend._byID_rel(rel_ids, return_dict=False,
416 eager_load = True, data = True,
417 thing_data = True)
418 rels = list(rels)
419 except NotFound:
420 if _update:
421 raise
422 else:
423 log_text("friend-rels-bandaid 1",
424 "Had to recalc friend_rels (1) for %s" % self.name,
425 "warning")
426 return self.friend_rels(_update=True)
427
428 if not _update:
429 sorted_1 = sorted([r._thing2_id for r in rels])
430 sorted_2 = sorted(list(self.friends))
431 if sorted_1 != sorted_2:
432 g.log.error("FR1: %r" % sorted_1)
433 g.log.error("FR2: %r" % sorted_2)
434 log_text("friend-rels-bandaid 2",
435 "Had to recalc friend_rels (2) for %s" % self.name,
436 "warning")
437 self.friend_ids(_update=True)
438 return self.friend_rels(_update=True)
0ae8f2f @ketralnis 21 Jul 2010 merge
ketralnis authored
439 return dict((r._thing2_id, r) for r in rels)
440
441 def add_friend_note(self, friend, note):
442 rels = self.friend_rels()
443 rel = rels[friend._id]
444 rel.note = note
445 rel._commit()
446
ea1fb6f Redesign the account deletion page.
Max Goodman authored
447 def delete(self, delete_message=None):
448 self.delete_message = delete_message
f1db8ef @atiaxi API: Record account deletion time
atiaxi authored
449 self.delete_time = datetime.now(g.tz)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
450 self._deleted = True
451 self._commit()
e35b519 * removed references to clear_memo
spez authored
452
453 #update caches
454 Account._by_name(self.name, allow_deleted = True, _update = True)
455 #we need to catch an exception here since it will have been
456 #recently deleted
457 try:
458 Account._by_name(self.name, _update = True)
459 except NotFound:
460 pass
0bceab8 @JordanMilne Add queue to scrub deleted accounts of potential leaks
JordanMilne authored
461
462 # Mark this account for scrubbing
463 amqp.add_item('account_deleted', self._fullname)
464
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
465 #remove from friends lists
466 q = Friend._query(Friend.c._thing2_id == self._id,
467 Friend.c._name == 'friend',
468 eager_load = True)
469 for f in q:
470 f._thing1.remove_friend(f._thing2)
471
4d7a2fa @kemitche Allow users to block users that harass them
kemitche authored
472 q = Friend._query(Friend.c._thing2_id == self._id,
473 Friend.c._name == 'enemy',
474 eager_load=True)
475 for f in q:
476 f._thing1.remove_enemy(f._thing2)
477
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
478 # wipe out stored password data after a recovery period
479 TryLater.schedule("account_deletion", self._id36,
480 delay=timedelta(days=90))
481
5765572 @dpifke Tweaks to OAuth2 models.
dpifke authored
482 # Remove OAuth2Client developer permissions. This will delete any
483 # clients for which this account is the sole developer.
d4d2214 Fix ImportError in Account.delete (oauth2 -> token).
Logan Hanks authored
484 from r2.models.token import OAuth2Client
5765572 @dpifke Tweaks to OAuth2 models.
dpifke authored
485 for client in OAuth2Client._by_developer(self):
486 client.remove_developer(self)
487
65b1467 @atiaxi Backend support for bans.
atiaxi authored
488 # 'State' bitfield properties
489 @property
490 def _banned(self):
491 return self.state & 1
492
493 @_banned.setter
494 def _banned(self, value):
495 if value and not self._banned:
496 self.state |= 1
497 # Invalidate all cookies by changing the password
498 # First back up the password so we can reverse this
499 self.backup_password = self.password
500 # New PW doesn't matter, they can't log in with it anyway.
501 # Even if their PW /was/ 'banned' for some reason, this
502 # will change the salt and thus invalidate the cookies
503 change_password(self, 'banned')
504
505 # deauthorize all access tokens
506 from r2.models.token import OAuth2AccessToken
507 from r2.models.token import OAuth2RefreshToken
508
509 OAuth2AccessToken.revoke_all_by_user(self)
510 OAuth2RefreshToken.revoke_all_by_user(self)
511 elif not value and self._banned:
512 self.state &= ~1
513
514 # Undo the password thing so they can log in
515 self.password = self.backup_password
516
517 # They're on their own for OAuth tokens, though.
518
519 self._commit()
520
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
521 @property
522 def subreddits(self):
523 from subreddit import Subreddit
524 return Subreddit.user_subreddits(self)
525
3735033 Add support for special user distingushes.
Max Goodman authored
526 def special_distinguish(self):
527 if self._t.get("special_distinguish_name"):
528 return dict((k, self._t.get("special_distinguish_"+k, None))
529 for k in ("name", "kind", "symbol", "cssclass", "label", "link"))
530 else:
531 return None
532
e87f520 @KeyserSosa New Features:
KeyserSosa authored
533 def quota_key(self, kind):
534 return "user_%s_quotas-%s" % (kind, self.name)
535
536 def clog_quota(self, kind, item):
537 key = self.quota_key(kind)
538 fnames = g.hardcache.get(key, [])
539 fnames.append(item._fullname)
540 g.hardcache.set(key, fnames, 86400 * 30)
541
542 def quota_baskets(self, kind):
543 from r2.models.admintools import filter_quotas
544 key = self.quota_key(kind)
545 fnames = g.hardcache.get(key)
546
547 if not fnames:
548 return None
549
550 unfiltered = Thing._by_fullname(fnames, data=True, return_dict=False)
551
552 baskets, new_quotas = filter_quotas(unfiltered)
553
554 if new_quotas is None:
555 pass
556 elif new_quotas == []:
557 g.hardcache.delete(key)
558 else:
559 g.hardcache.set(key, new_quotas, 86400 * 30)
560
561 return baskets
562
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
563 # Needs to take the *canonicalized* version of each email
564 # When true, returns the reason
565 @classmethod
566 def which_emails_are_banned(cls, canons):
3437713 @atiaxi Email: Use non-hardcache ban fetching system
atiaxi authored
567 banned = hooks.get_hook('email.get_banned').call(canons=canons)
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
568
3437713 @atiaxi Email: Use non-hardcache ban fetching system
atiaxi authored
569 # Create a dictionary like:
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
570 # d["abc.def.com"] = [ "bob@abc.def.com", "sue@abc.def.com" ]
571 rv = {}
572 canons_by_domain = {}
3437713 @atiaxi Email: Use non-hardcache ban fetching system
atiaxi authored
573
574 # email.get_banned will return a list of lists (one layer from the
575 # hooks system, the second from the function itself); chain them
576 # together for easy processing
577 for canon in itertools.chain(*banned):
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
578 rv[canon] = None
579
580 at_sign = canon.find("@")
581 domain = canon[at_sign+1:]
582 canons_by_domain.setdefault(domain, [])
583 canons_by_domain[domain].append(canon)
584
48dfdfc @atiaxi Domainban: Stop depending on zookeeper
atiaxi authored
585 # Hand off to the domain ban system; it knows in the case of
586 # abc@foo.bar.com to check foo.bar.com, bar.com, and .com
587 from r2.models.admintools import bans_for_domain_parts
588
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
589 for domain, canons in canons_by_domain.iteritems():
48dfdfc @atiaxi Domainban: Stop depending on zookeeper
atiaxi authored
590 for d in bans_for_domain_parts(domain):
591 if d.no_email:
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
592 rv[canon] = "domain"
593
594 return rv
595
003ca6e @bsimpson63 Add AccountsByCanonicalEmail.
bsimpson63 authored
596 def set_email(self, email):
597 old_email = self.email
598 self.email = email
599 self._commit()
600 AccountsByCanonicalEmail.update_email(self, old_email, email)
601
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
602 def has_banned_email(self):
603 canon = self.canonical_email()
604 which = self.which_emails_are_banned((canon,))
605 return which.get(canon, None)
606
d251ba7 @KeyserSosa * Improvements to the email verification system
KeyserSosa authored
607 def canonical_email(self):
76594cd @spladug Refactor and reorganize email canonicalization; add some tests.
spladug authored
608 return canonicalize_email(self.email)
d251ba7 @KeyserSosa * Improvements to the email verification system
KeyserSosa authored
609
610 def cromulent(self):
611 """Return whether the user has validated their email address and
612 passes some rudimentary 'not evil' checks."""
613
614 if not self.email_verified:
615 return False
616
1d9b9fe @KeyserSosa * transparency updates to some of the pngs thanks to ytknows
KeyserSosa authored
617 if self.has_banned_email():
d251ba7 @KeyserSosa * Improvements to the email verification system
KeyserSosa authored
618 return False
619
620 # Otherwise, congratulations; you're cromulent!
621 return True
622
e87f520 @KeyserSosa New Features:
KeyserSosa authored
623 def quota_limits(self, kind):
624 if kind != 'link':
625 raise NotImplementedError
626
d251ba7 @KeyserSosa * Improvements to the email verification system
KeyserSosa authored
627 if self.cromulent():
e87f520 @KeyserSosa New Features:
KeyserSosa authored
628 return dict(hour=3, day=10, week=50, month=150)
629 else:
630 return dict(hour=1, day=3, week=5, month=5)
631
632 def quota_full(self, kind):
633 limits = self.quota_limits(kind)
634 baskets = self.quota_baskets(kind)
635
636 if baskets is None:
637 return None
638
639 total = 0
640 filled_quota = None
641 for key in ('hour', 'day', 'week', 'month'):
642 total += len(baskets[key])
643 if total >= limits[key]:
644 filled_quota = key
645
646 return filled_quota
647
2869eaf @ketralnis New features:
ketralnis authored
648 @classmethod
e87f520 @KeyserSosa New Features:
KeyserSosa authored
649 def system_user(cls):
650 try:
651 return cls._by_name(g.system_user)
52da322 @KeyserSosa Bugfixes:
KeyserSosa authored
652 except (NotFound, AttributeError):
e87f520 @KeyserSosa New Features:
KeyserSosa authored
653 return None
a402d48 New features:
Mike authored
654
4631751 User option to block flair, and layout fixes.
Logan Hanks authored
655 def flair_enabled_in_sr(self, sr_id):
30c9134 Tweak flair preference lookup to not break on multi ids.
Max Goodman authored
656 return getattr(self, 'flair_%s_enabled' % sr_id, True)
4631751 User option to block flair, and layout fixes.
Logan Hanks authored
657
d6f2574 @bsimpson63 Cleanup wrapped_flair.
bsimpson63 authored
658 def flair_text(self, sr_id):
659 return getattr(self, 'flair_%s_text' % sr_id, None)
660
661 def flair_css_class(self, sr_id):
662 return getattr(self, 'flair_%s_css_class' % sr_id, None)
663
b24ba98 @Deimos Refactor flair API endpoints
Deimos authored
664 def can_flair_in_sr(self, user, sr):
665 """Return whether a user can set this one's flair in a subreddit."""
666 can_assign_own = self._id == user._id and sr.flair_self_assign_enabled
667
668 return can_assign_own or sr.is_moderator_with_perms(user, "flair")
669
670 def set_flair(self, subreddit, text=None, css_class=None, set_by=None,
671 log_details="edit"):
672 log_details = "flair_%s" % log_details
673 if not text and not css_class:
674 # set to None instead of potentially empty strings
675 text = css_class = None
676 subreddit.remove_flair(self)
677 log_details = "flair_delete"
678 elif not subreddit.is_flair(self):
679 subreddit.add_flair(self)
680
681 setattr(self, 'flair_%s_text' % subreddit._id, text)
682 setattr(self, 'flair_%s_css_class' % subreddit._id, css_class)
683 self._commit()
684
685 if set_by and set_by != self:
686 ModAction.create(subreddit, set_by, action='editflair',
687 target=self, details=log_details)
688
7e22528 @alienth Add Account method to update AccountActivityBySR.
alienth authored
689 def update_sr_activity(self, sr):
690 if not self._spam:
1520ddf @alienth Rename AccountActivityBySR to AccountsActiveBySR.
alienth authored
691 AccountsActiveBySR.touch(self, sr)
7e22528 @alienth Add Account method to update AccountActivityBySR.
alienth authored
692
fc9abd1 @kemitche Award-claiming via one-time links.
kemitche authored
693 def get_trophy_id(self, uid):
694 '''Return the ID of the Trophy associated with the given "uid"
695
696 `uid` - The unique identifier for the Trophy to look up
697
698 '''
699 return getattr(self, 'received_trophy_%s' % uid, None)
700
701 def set_trophy_id(self, uid, trophy_id):
702 '''Recored that a user has received a Trophy with "uid"
703
704 `uid` - The trophy "type" that the user should only have one of
705 `trophy_id` - The ID of the corresponding Trophy object
706
707 '''
708 return setattr(self, 'received_trophy_%s' % uid, trophy_id)
709
75da617 @rram account: Add employee property.
rram authored
710 @property
711 def employee(self):
712 """Return if the user is an employee.
713
714 Being an employee grants them various special privileges.
715
716 """
dd925f4 @rram account: Protect employees against logged out users.
rram authored
717 return (hasattr(self, 'name') and
718 (self.name in g.admins or
719 self.name in g.sponsors or
720 self.name in g.employees))
75da617 @rram account: Add employee property.
rram authored
721
e394579 @bsimpson63 Per user CPM overrides.
bsimpson63 authored
722 @property
9e20cc1 @JordanMilne Allow forcing HTTPS upon employees
JordanMilne authored
723 def https_forced(self):
724 """Return whether this account may only be used via HTTPS."""
25a890d @JordanMilne Replace `disable_require_employee_https` with a feature flag
JordanMilne authored
725 if feature.is_enabled_for("require_https", self):
9e20cc1 @JordanMilne Allow forcing HTTPS upon employees
JordanMilne authored
726 return True
727 return self.pref_force_https
728
729 @property
8aa2312 @JordanMilne Disable toolbar when using forcing HTTPS
JordanMilne authored
730 def uses_toolbar(self):
731 return not self.https_forced and self.pref_frame
732
733 @property
19d9968 @bsimpson63 Don't PM users with gold subscription about expiration.
bsimpson63 authored
734 def has_gold_subscription(self):
210dd1b @bsimpson63 Store stripe customer id in Account.gold_subscr_id.
bsimpson63 authored
735 return bool(getattr(self, 'gold_subscr_id', None))
736
737 @property
738 def has_paypal_subscription(self):
739 return (self.has_gold_subscription and
740 not self.gold_subscr_id.startswith('cus_'))
741
742 @property
743 def has_stripe_subscription(self):
744 return (self.has_gold_subscription and
745 self.gold_subscr_id.startswith('cus_'))
19d9968 @bsimpson63 Don't PM users with gold subscription about expiration.
bsimpson63 authored
746
82b870d @Deimos Gold: add preference to auto-renew with a creddit
Deimos authored
747 @property
748 def gold_will_autorenew(self):
749 return (self.has_gold_subscription or
750 (self.pref_creddit_autorenew and self.gold_creddits > 0))
751
52d419f @xiongchiamiov Comment sort preference: dual-write to migrate
xiongchiamiov authored
752 @property
753 def default_comment_sort(self):
754 if self.pref_default_comment_sort:
755 return self.pref_default_comment_sort
756
757 old_sort_pref = self.sort_options.get('front_sort')
758 if old_sort_pref:
759 return old_sort_pref
760
761 return 'confidence'
762
e394579 @bsimpson63 Per user CPM overrides.
bsimpson63 authored
763
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
764 class FakeAccount(Account):
765 _nodb = True
5ef76b9 @KeyserSosa New features:
KeyserSosa authored
766 pref_no_profanity = True
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
767
b06dc9b Add equality implementation for Account model.
Max Goodman authored
768 def __eq__(self, other):
769 return self is other
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
770
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
771 def valid_admin_cookie(cookie):
772 if g.read_only_mode:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
773 return (False, None)
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
774
775 # parse the cookie
776 try:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
777 first_login, last_request, hash = cookie.split(',')
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
778 except ValueError:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
779 return (False, None)
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
780
781 # make sure it's a recent cookie
782 try:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
783 first_login_time = datetime.strptime(first_login, COOKIE_TIMESTAMP_FORMAT)
784 last_request_time = datetime.strptime(last_request, COOKIE_TIMESTAMP_FORMAT)
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
785 except ValueError:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
786 return (False, None)
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
787
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
788 cookie_age = datetime.utcnow() - first_login_time
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
789 if cookie_age.total_seconds() > g.ADMIN_COOKIE_TTL:
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
790 return (False, None)
791
792 idle_time = datetime.utcnow() - last_request_time
793 if idle_time.total_seconds() > g.ADMIN_COOKIE_MAX_IDLE:
794 return (False, None)
33b15bc @spladug Split the admin cookie out from the session cookie.
spladug authored
795
796 # validate
a42505c @spladug Keep admin cookie around if actively used.
spladug authored
797 expected_cookie = c.user.make_admin_cookie(first_login, last_request)
798 return (constant_time_compare(cookie, expected_cookie),
799 first_login)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
800
801
8dfd73b @spladug Add framework for RFC-6238: Time-Based One Time Password Algorithm.
spladug authored
802 def valid_otp_cookie(cookie):
803 if g.read_only_mode:
804 return False
805
806 # parse the cookie
807 try:
808 remembered_at, signature = cookie.split(",")
809 except ValueError:
810 return False
811
812 # make sure it hasn't expired
813 try:
814 remembered_at_time = datetime.strptime(remembered_at, COOKIE_TIMESTAMP_FORMAT)
815 except ValueError:
816 return False
817
818 age = datetime.utcnow() - remembered_at_time
819 if age.total_seconds() > g.OTP_COOKIE_TTL:
820 return False
821
822 # validate
823 expected_cookie = c.user.make_otp_cookie(remembered_at)
824 return constant_time_compare(cookie, expected_cookie)
825
826
a402d48 New features:
Mike authored
827 def valid_feed(name, feedhash, path):
828 if name and feedhash and path:
829 from r2.lib.template_helpers import add_sr
830 path = add_sr(path)
831 try:
832 user = Account._by_name(name)
833 if (user.pref_private_feeds and
83058d4 @ekarulf Use constant-time string comparison for auth.
ekarulf authored
834 constant_time_compare(feedhash, make_feedhash(user, path))):
a402d48 New features:
Mike authored
835 return user
836 except NotFound:
837 pass
838
839 def make_feedhash(user, path):
3366083 @spladug Create a vault for secret tokens and move some into it.
spladug authored
840 return hashlib.sha1("".join([user.name, user.password,
841 g.secrets["FEEDSECRET"]])
a402d48 New features:
Mike authored
842 ).hexdigest()
843
844 def make_feedurl(user, path, ext = "rss"):
845 u = UrlParser(path)
846 u.update_query(user = user.name,
847 feed = make_feedhash(user, path))
848 u.set_extension(ext)
849 return u.unparse()
850
722fbf2 @atiaxi Passwords: Enable checks against other passwords
atiaxi authored
851 def valid_password(a, password, compare_password=None):
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
852 # bail out early if the account or password's invalid
853 if not hasattr(a, 'name') or not hasattr(a, 'password') or not password:
854 return False
855
722fbf2 @atiaxi Passwords: Enable checks against other passwords
atiaxi authored
856 convert_password = False
857 if compare_password is None:
858 convert_password = True
859 compare_password = a.password
860
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
861 # standardize on utf-8 encoding
862 password = filters._force_utf8(password)
863
722fbf2 @atiaxi Passwords: Enable checks against other passwords
atiaxi authored
864 if compare_password.startswith('$2a$'):
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
865 # it's bcrypt.
722fbf2 @atiaxi Passwords: Enable checks against other passwords
atiaxi authored
866 expected_hash = bcrypt.hashpw(password, compare_password)
867 if not constant_time_compare(compare_password, expected_hash):
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
868 return False
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
869
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
870 # if it's using the current work factor, we're done, but if it's not
871 # we'll have to rehash.
872 # the format is $2a$workfactor$salt+hash
722fbf2 @atiaxi Passwords: Enable checks against other passwords
atiaxi authored
873 work_factor = int(compare_password.split("$")[2])
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
874 if work_factor == g.bcrypt_work_factor:
875 return a
876 else:
877 # alright, so it's not bcrypt. how old is it?
878 # if the length of the stored hash is 43 bytes, the sha-1 hash has a salt
879 # otherwise it's sha-1 with no salt.
880 salt = ''
722fbf2 @atiaxi Passwords: Enable checks against other passwords
atiaxi authored
881 if len(compare_password) == 43:
882 salt = compare_password[:3]
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
883 expected_hash = passhash(a.name, password, salt)
884
722fbf2 @atiaxi Passwords: Enable checks against other passwords
atiaxi authored
885 if not constant_time_compare(compare_password, expected_hash):
a44f6f4 @spladug Upgrade passwords on log in when bcrypt work factor changed.
spladug authored
886 return False
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
887
888 # since we got this far, it's a valid password but in an old format
889 # let's upgrade it
722fbf2 @atiaxi Passwords: Enable checks against other passwords
atiaxi authored
890 if convert_password:
891 a.password = bcrypt_password(password)
892 a._commit()
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
893 return a
894
895 def bcrypt_password(password):
896 salt = bcrypt.gensalt(log_rounds=g.bcrypt_work_factor)
897 return bcrypt.hashpw(password, salt)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
898
899 def passhash(username, password, salt = ''):
900 if salt is True:
901 salt = randstr(3)
902 tohash = '%s%s %s' % (salt, username, password)
c9c65dd @spladug Replace references to deprecated sha module with hashlib.
spladug authored
903 return salt + hashlib.sha1(tohash).hexdigest()
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
904
c852953 fixed recover password
shuffman authored
905 def change_password(user, newpassword):
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
906 user.password = bcrypt_password(newpassword)
c852953 fixed recover password
shuffman authored
907 user._commit()
abf6f6e @alienth Store a LastModified update on account password change.
alienth authored
908 LastModified.touch(user._fullname, 'Password')
c852953 fixed recover password
shuffman authored
909 return True
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
910
911 #TODO reset the cache
b55934a @spladug Account: ensure all non-defaulted attributes are atomically created.
spladug authored
912 def register(name, password, registration_ip):
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
913 try:
914 a = Account._by_name(name)
915 raise AccountExists
916 except NotFound:
917 a = Account(name = name,
a311805 @spladug Switch to bcrypt for password hashing.
spladug authored
918 password = bcrypt_password(password))
5ef76b9 @KeyserSosa New features:
KeyserSosa authored
919 # new accounts keep the profanity filter settings until opting out
920 a.pref_no_profanity = True
b55934a @spladug Account: ensure all non-defaulted attributes are atomically created.
spladug authored
921 a.registration_ip = registration_ip
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
922 a._commit()
e35b519 * removed references to clear_memo
spez authored
923
924 #clear the caches
925 Account._by_name(name, _update = True)
926 Account._by_name(name, allow_deleted = True, _update = True)
4778b17 @KeyserSosa initial checkin
KeyserSosa authored
927 return a
928
929 class Friend(Relation(Account, Account)): pass
bf9f43c @KeyserSosa Messaging/commenting
KeyserSosa authored
930
4d7a2fa @kemitche Allow users to block users that harass them
kemitche authored
931 Account.__bases__ += (UserRel('friend', Friend, disable_reverse_ids_fn=True),
6c10091 @kemitche Reverse lookup of blocked user
kemitche authored
932 UserRel('enemy', Friend, disable_reverse_ids_fn=False))
4440ccf @KeyserSosa overhaul of JS and form handling code, not based on jQuery
KeyserSosa authored
933
934 class DeletedUser(FakeAccount):
935 @property
936 def name(self):
937 return '[deleted]'
938
a402d48 New features:
Mike authored
939 @property
940 def _deleted(self):
941 return True
942
4440ccf @KeyserSosa overhaul of JS and form handling code, not based on jQuery
KeyserSosa authored
943 def _fullname(self):
944 raise NotImplementedError
945
946 def _id(self):
947 raise NotImplementedError
a402d48 New features:
Mike authored
948
949 def __setattr__(self, attr, val):
950 if attr == '_deleted':
951 pass
952 else:
953 object.__setattr__(self, attr, val)
2fe83ed @alienth Add AccountActivityBySR.
alienth authored
954
1520ddf @alienth Rename AccountActivityBySR to AccountsActiveBySR.
alienth authored
955 class AccountsActiveBySR(tdb_cassandra.View):
2fe83ed @alienth Add AccountActivityBySR.
alienth authored
956 _use_db = True
957 _connection_pool = 'main'
6bfa552 @kemitche tdb_cassandra: Magic up the _ttl attribute for ThingMeta users
kemitche authored
958 _ttl = timedelta(minutes=15)
2fe83ed @alienth Add AccountActivityBySR.
alienth authored
959
960 _extra_schema_creation_args = dict(key_validation_class=ASCII_TYPE)
961
962 _read_consistency_level = tdb_cassandra.CL.ONE
963 _write_consistency_level = tdb_cassandra.CL.ANY
964
965 @classmethod
966 def touch(cls, account, sr):
967 cls._set_values(sr._id36,
968 {account._id36: ''})
969
970 @classmethod
49607b7 @alienth Add a memoized method of get_count, and use it by default.
alienth authored
971 def get_count(cls, sr, cached=True):
972 return cls.get_count_cached(sr._id36, _update=not cached)
973
974 @classmethod
975 @memoize('accounts_active', time=60)
976 def get_count_cached(cls, sr_id):
977 return cls._cf.get_count(sr_id)
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
978
979
ff5e5e0 @bsimpson63 Let users block messages from subreddits.
bsimpson63 authored
980 class BlockedSubredditsByAccount(tdb_cassandra.DenormalizedRelation):
981 _use_db = True
982 _last_modified_name = 'block_subreddit'
983 _read_consistency_level = tdb_cassandra.CL.QUORUM
984 _write_consistency_level = tdb_cassandra.CL.QUORUM
985 _connection_pool = 'main'
986 _views = []
987
988 @classmethod
989 def value_for(cls, thing1, thing2):
990 return ''
991
992 @classmethod
993 def block(cls, user, sr):
994 cls.create(user, sr)
995
996 @classmethod
997 def unblock(cls, user, sr):
998 cls.destroy(user, sr)
999
1000 @classmethod
1001 def is_blocked(cls, user, sr):
1002 try:
df77799 @bsimpson63 BlockedSubredditsByAccount: fix is_blocked method.
bsimpson63 authored
1003 r = cls.fast_query(user, [sr])
ff5e5e0 @bsimpson63 Let users block messages from subreddits.
bsimpson63 authored
1004 except tdb_cassandra.NotFound:
1005 return False
df77799 @bsimpson63 BlockedSubredditsByAccount: fix is_blocked method.
bsimpson63 authored
1006 return (user, sr) in r
ff5e5e0 @bsimpson63 Let users block messages from subreddits.
bsimpson63 authored
1007
1008
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
1009 @trylater_hooks.on("trylater.account_deletion")
fb507e5 @xiongchiamiov TryLater: use more generic parameter name
xiongchiamiov authored
1010 def on_account_deletion(data):
1011 for account_id36 in data.itervalues():
d2ccc40 @spladug Automatically delete password hashes of deleted accounts.
spladug authored
1012 account = Account._byID36(account_id36, data=True)
1013
1014 if not account._deleted:
1015 continue
1016
1017 account.password = ""
1018 account._commit()
003ca6e @bsimpson63 Add AccountsByCanonicalEmail.
bsimpson63 authored
1019
1020
1021 class AccountsByCanonicalEmail(tdb_cassandra.View):
1022 __metaclass__ = tdb_cassandra.ThingMeta
1023
1024 _use_db = True
1025 _compare_with = tdb_cassandra.UTF8_TYPE
1026 _extra_schema_creation_args = dict(
1027 key_validation_class=tdb_cassandra.UTF8_TYPE,
1028 )
1029
1030 @classmethod
1031 def update_email(cls, account, old, new):
1032 old, new = map(canonicalize_email, (old, new))
1033
1034 if old == new:
1035 return
1036
1037 with cls._cf.batch() as b:
1038 if old:
1039 b.remove(old, {account._id36: ""})
1040 if new:
1041 b.insert(new, {account._id36: ""})
1042
1043 @classmethod
1044 def get_accounts(cls, email_address):
1045 canonical = canonicalize_email(email_address)
1046 if not canonical:
1047 return []
1048 account_id36s = cls.get_time_sorted_columns(canonical).keys()
1049 return Account._byID36(account_id36s, data=True, return_dict=False)
Something went wrong with that request. Please try again.