Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IA-only auth flow #526

Merged
merged 8 commits into from
Aug 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
350 changes: 103 additions & 247 deletions openlibrary/accounts/model.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions openlibrary/core/lending.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def setup(config):
config_internal_tests_api_key = config.get('internal_tests_api_key')
config_http_request_timeout = config.get('http_request_timeout')


def is_borrowable(identifiers, acs=False, restricted=False):
"""Takes a list of archive.org ocaids and returns json indicating
whether each of these books represented by these identifiers are
Expand Down
136 changes: 47 additions & 89 deletions openlibrary/plugins/upstream/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from openlibrary import accounts
from openlibrary.accounts import (
audit_accounts, link_accounts, create_accounts,
audit_accounts,
Account, OpenLibraryAccount, InternetArchiveAccount,
valid_email
)
Expand All @@ -39,6 +39,22 @@
create_link_doc = accounts.create_link_doc
sendmail = accounts.sendmail

LOGIN_ERRORS = {
"invalid_email": "The email address you entered is invalid",
"account_blocked": "This account has been blocked",
"account_locked": "This account has been blocked",
"account_not_found": "No account was found with this email. Please try again",
"account_incorrect_password": "The password you entered is incorrect. Please try again",
"account_bad_password": "Wrong password. Please try again",
"account_not_verified": "Please verify your Open Library account before logging in",
"ia_account_not_verified": "Please verify your Internet Archive account before logging in",
"missing_fields": "Please fill out all fields and try again",
"email_registered": "This email is already registered",
"username_registered": "This username is already registered",
"ia_login_only": "Sorry, you must use your Internet Archive email and password to log in",
"max_retries_exceeded": "A problem occurred and we were unable to log you in.",
"wrong_ia_account": "An Open Library account with this email is already linked to a different Internet Archive account. Please contact info@openlibrary.org."
}

class availability(delegate.page):
path = "/internal/fake/availability"
Expand Down Expand Up @@ -176,7 +192,7 @@ def GET(self):
class account_create(delegate.page):
"""New account creation.

Account will in the pending state until the email is activated.
Account remains in the pending state until the email is activated.
"""
path = "/account/create"

Expand Down Expand Up @@ -213,16 +229,23 @@ def POST(self):
f.note = utils.get_error("account_create_tos_not_selected")
return render['account/create'](f)

ia_account = InternetArchiveAccount.get(email=i.email)
# Require email to not already be used in IA or OL
if ia_account:
f.note = LOGIN_ERRORS['email_registered']
return render['account/create'](f)

try:
accounts.register(username=i.username,
email=i.email,
password=i.password,
displayname=i.displayname)
except ClientException, e:
f.note = str(e)
# Create ia_account: require they activate via IA email
# and then login to OL. Logging in after activation with
# IA credentials will auto create and link OL account.
ia_account = InternetArchiveAccount.create(
screenname=i.username, email=i.email, password=i.password,
verified=False)
except ValueError as e:
f.note = LOGIN_ERRORS['max_retries_exceeded']
return render['account/create'](f)

send_verification_email(i.username, i.email)
return render['account/verify'](username=i.username, email=i.email)

del delegate.pages['/account/register']
Expand All @@ -238,6 +261,12 @@ class account_login(delegate.page):
"""
path = "/account/login"

def render_error(self, error_key, i):
f = forms.Login()
f.fill(i)
f.note = LOGIN_ERRORS[error_key]
return render.login(f)

def GET(self):
referer = web.ctx.env.get('HTTP_REFERER', '/')
i = web.input(redirect=referer)
Expand All @@ -246,45 +275,14 @@ def GET(self):
return render.login(f)

def POST(self):
i = web.input(email='', connect=None, remember=False,
redirect='/', action="login")

if i.action == "resend_verification_email":
return self.POST_resend_verification_email(i)
else:
return self.POST_login(i)

def error(self, name, i):
f = forms.Login()
f.fill(i)
f.note = utils.get_error(name)
return render.login(f)

def error_check(self, audit, i):
if 'error' in audit:
error = audit['error']
if error == "account_not_verified":
return render_template(
"account/not_verified", username=account.username,
password=i.password, email=account.email)
elif error == "account_not_found":
return self.error("account_user_notfound", i)
elif error == "account_blocked":
return self.error("account_blocked", i)
else:
return self.error(audit['error'], i)
if not audit['link']:
# This needs to be overriden w/ `test`
return self.error("accounts_not_connected", i)
return None

def POST_login(self, i):
i = web.input(username="", password="", remember=False, redirect='')
i = web.input(username="", connect=None, password="", remember=False,
redirect='/', test=False)
email = i.username # XXX username is now email
audit = audit_accounts(email, i.password, require_link=True, test=i.test)
error = audit.get('error')

audit = audit_accounts(i.username, i.password)
errors = self.error_check(audit, i)
if errors:
return errors
if error:
return self.render_error(error, i)

blacklist = ["/account/login", "/account/password", "/account/email",
"/account/create"]
Expand All @@ -298,13 +296,13 @@ def POST_login(self, i):

def POST_resend_verification_email(self, i):
try:
accounts.login(i.username, i.password)
ol_login = OpenLibraryAccount.authenticate(i.email, i.password)
except ClientException, e:
code = e.get_data().get("code")
if code != "account_not_verified":
return self.error("account_incorrect_password", i)

account = accounts.find(username=i.username)
account = OpenLibraryAccount.get(email=i.email)
account.send_verification_email()

title = _("Hi %(user)s", user=account.displayname)
Expand Down Expand Up @@ -367,11 +365,6 @@ def get_email(self):
user = accounts.get_current_user()
return user.get_account()['email']

@require_login
def GET(self):
f = forms.ChangeEmail()
return render['account/email'](self.get_email(), f)

@require_login
def POST(self):
f = forms.ChangeEmail()
Expand Down Expand Up @@ -566,41 +559,6 @@ def POST(self, code):
return render_template("account/password/reset_success", username=username)


class account_connect(delegate.page):

path = "/account/connect"

def POST(self):
"""When a user logs in with either an OL or IA account which have not
been linked, and if the user's credentials for this account
have been verified, the next step is for the user to (a)
connect their account to an account for whichever service is
missing, or (b) to create a new account for this service and
then link them. The /account/connect endpoint handles this
linking case and dispatches to the correct method (either
'link' or 'create' depending on the parameters POSTed to the
endpoint).

Note: Emails are case sensitive behind the scenes and
functions which require them as lower will make them so
"""

i = web.input(email="", password="", username="",
bridgeEmail="", bridgePassword="",
token="", service="link")
test = 'openlibrary' if i.token == lending.config_internal_tests_api_key else None
if i.service == "link":
result = link_accounts(i.get('email'), i.password,
bridgeEmail=i.bridgeEmail,
bridgePassword=i.bridgePassword)
elif i.service == "create":
result = create_accounts(i.get('email'), i.password,
username=i.username, test=test)
else:
result = {'error': 'invalid_option'}
return delegate.RawText(simplejson.dumps(result),
content_type="application/json")

class account_audit(delegate.page):

path = "/account/audit"
Expand Down
22 changes: 13 additions & 9 deletions openlibrary/plugins/upstream/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
from openlibrary.i18n import lgettext as _
from openlibrary.utils.form import Form, Textbox, Password, Hidden, Validator, RegexpValidator
from openlibrary import accounts
from openlibrary.accounts import InternetArchiveAccount
from . import spamcheck

def find_account(username=None, lusername=None, email=None):
return accounts.find(username=username, lusername=lusername, email=email)

def find_ia_account(email=None):
ia_account = InternetArchiveAccount.get(email=email)
return ia_account

Login = Form(
Textbox('username', description=_('Username'), klass='required'),
Password('password', description=_('Password'), klass='required'),
Expand All @@ -18,7 +23,7 @@ def find_account(username=None, lusername=None, email=None):
forms.login = Login

email_already_used = Validator(_("No user registered with this email address"), lambda email: find_account(email=email) is not None)
email_not_already_used = Validator(_("Email already used"), lambda email: find_account(email=email) is None)
email_not_already_used = Validator(_("Email already registered"), lambda email: not find_ia_account(email=email))
email_not_disposable = Validator(_("Disposable email not permitted"), lambda email: not email.lower().endswith('dispostable.com'))
email_domain_not_blocked = Validator(_("Your email provider is not recognized."), lambda email: not spamcheck.is_spam_email(email))
username_validator = Validator(_("Username already used"), lambda username: not find_account(lusername=username.lower()))
Expand All @@ -39,20 +44,19 @@ def valid(self, value):

class RegisterForm(Form):
INPUTS = [
Textbox("displayname", description=_("Your Full Name")),
Textbox('email', description=_('Your Email Address'),
Textbox('email', description=_('Your email address'),
klass='required',
validators=[vemail, email_not_already_used, email_not_disposable, email_domain_not_blocked]),
Textbox('email2', description=_('Confirm Your Email Address'),
klass='required',
validators=[EqualToValidator('email', _('Your emails do not match. Please try again.'))]),
Textbox('username', description=_('Choose a Username'),
Textbox('username', description=_('Choose a screen name'),
klass='required',
help=_("Only letters and numbers, please, and at least 3 characters."),
validators=[vlogin, username_validator]),
Password('password', description=_('Choose a Password'),
Password('password', description=_('Choose a password'),
klass='required',
validators=[vpass]),
Textbox('password2', description=_('Confirm password'),
klass='required',
validators=[vpass])
validators=[vpass, EqualToValidator('password', _("Passwords didn't match."))]),
]
def __init__(self):
Form.__init__(self, *self.INPUTS)
Expand Down
13 changes: 3 additions & 10 deletions openlibrary/plugins/upstream/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,17 @@
from .. import spamcheck

class TestRegister:
def test_leaks(self):
f = forms.Register()
assert f.displayname.value is None
f.displayname.value = 'Foo'

f = forms.Register()
assert f.displayname.value is None

def test_validate(self, monkeypatch):
monkeypatch.setattr(forms, 'find_account', lambda **kw: None)
monkeypatch.setattr(forms, 'find_ia_account', lambda **kw: None)
monkeypatch.setattr(spamcheck, "get_spam_domains", lambda: [])

f = forms.Register()
d = {
'displayname': 'Foo',
'username': 'foo',
'email': 'foo@example.com',
'email2': 'foo@example.com',
'password': 'foo123'
'password': 'foo123',
'password2': 'foo123'
}
assert f.validates(d)
7 changes: 2 additions & 5 deletions openlibrary/templates/account.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,12 @@ <h1>$_("Settings")</h1>
</div>

<div id="contentBody">
<p class="sansserif larger"><a href="/account/password">$_("Change Password")</a></p>
<p class="sansserif larger"><a href="/account/email">$_("Update Email Address")</a></p>
<p class="sansserif larger"><a href="https://archive.org/account/index.php?settings=1">$_("Change Password")</a></p>
<p class="sansserif larger"><a href="https://archive.org/account/index.php?settings=1">$_("Update Email Address")</a></p>
<p class="sansserif larger"><a href="/account/notifications">$_("Your Notifications")</a></p>
<p class="sansserif larger"><a href="$user.key">$_("View")</a> $_("or") <a href="$user.key?m=edit">$_("Edit")</a> $_("Your Profile Page")</p>
<p class="sansserif larger"><a href="/account/lists">$("View or edit your Lists")</a></p>
<br/>
<!--
<p class="sansserif larger"><a href="/account/delete" class="fixthis">$_("Delete Your Account")</a> <i>coming soon...</i></p>
-->
<p class="sansserif larger"><a href="/contact">$_("Please contact us if you need help with anything else.")</a></p>

</div>
Expand Down
39 changes: 15 additions & 24 deletions openlibrary/templates/account/create.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<div id="contentHead">
<h1>$_("Sign Up")</h1>
$if not ctx.user:
<p class="instruct">$_("Complete the form below to create your new Open Library account.")
<span class="attn remind">$_("Each field is required, and you'll need to verify your email address.")</span></p>
<p class="instruct">$_("Complete the form below to create a new Internet Archive account.")
<span class="attn remind">$_("Each field is required")</span></p>
</div>

<div id="contentBody">
Expand All @@ -28,30 +28,31 @@ <h1>$_("Sign Up")</h1>
<form id="signup" class="olform create validate" name="signup" method="post" action="">
$if form.note:
<div class="note">$form.note</div>

<div class="formElement">
<div class="label">
<label for="displayname">$_("Your Full Name")</label>
<span class="smaller lighter">$_("This is how your additions and edits will be credited on the site.")</span>
<label for="emailAddr">$("Your email address")</label>
<span class="smaller lighter"></span>
</div>
<div class="input">
<input type="text" class="required" name="displayname" id="displayname" value="$form.displayname.value"/>
<span class="invalid clearfix" htmlfor="username">$form.displayname.note</span>
<input type="text" class="required email" name="email" id="emailAddr" value="$form.email.value"/>
<span class="invalid clearfix" htmlfor="email">$form.email.note</span>
</div>
</div>
<div class="formElement">
<div class="label">
<label for="username">$_("Choose a Username")</label>
<label for="username">$_("Choose a screen name")</label>
<span class="smaller lighter">$_("Letters and numbers only please, and at least 3 characters.")</span>
</div>
<div class="input">
<input type="text" class="required" name="username" id="username" value="$form.username.value" autocapitalize="off"/>
<span class="invalid clearfix" htmlfor="username">$form.username.note</span>
<div class="sansserif smaller lighter">Your URL: https://openlibrary.org/people/<span id="userUrl">username</span></div>
<div class="sansserif smaller lighter">Your URL: https://openlibrary.org/people/<span id="userUrl">screenname</span></div>
</div>
</div>
<div class="formElement">
<div class="label">
<label for="password">$("Choose a Password")</label>
<label for="password">$("Choose a password")</label>
<span class="smaller lighter"></span></div>
<div class="input">
<input value="" type="password" class="required" name="password" id="password"/>
Expand All @@ -60,24 +61,14 @@ <h1>$_("Sign Up")</h1>
</div>
<div class="formElement">
<div class="label">
<label for="emailAddr">$("Your Email Address")</label>
<span class="smaller lighter"></span>
</div>
<div class="input">
<input type="text" class="required email" name="email" id="emailAddr" value="$form.email.value"/>
<span class="invalid clearfix" htmlfor="email">$form.email.note</span>
</div>
</div>
<div class="formElement">
<div class="label">
<label for="emailAddr2">$("Confirm Your Email Address")</label>
<span class="smaller lighter"></span>
</div>
<label for="password">$("Confirm password")</label>
<span class="smaller lighter"></span></div>
<div class="input">
<input type="text" class="required email" name="email2" id="emailAddr2" value="$form.email2.value"/>
<span class="invalid clearfix" htmlfor="email2">$form.email2.note</span>
<input value="" type="password" class="required" name="password2" id="password2"/>
<span class="invalid clearfix" htmlfor="password2">$form.password2.note</span>
</div>
</div>

$if form.has_recaptcha:
<div class="formElement">
<div class="label">$_("Please type in the text or number(s) below.")<br/><span class="smaller lighter">$_("If you have security settings or privacy blockers installed, please disable them to see the reCAPTCHA.")</span></div>
Expand Down
Loading