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

Verify admin email address #3267

Merged
merged 33 commits into from
Jan 13, 2019
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
35a8cd5
add an bit
Jan 9, 2019
1d1564d
prompt on homepage when user's email hasn't been verified
Jan 9, 2019
44949da
set e-mail as verified for new setups and invited users
Jan 9, 2019
b186979
👋 copy & paste invite links, it's time for verified e-mails!
Jan 9, 2019
693b384
default `is_invitation_pending` to false and actively set it to true
Jan 9, 2019
f614dea
fix tests that broke due to default is_invitation_pending value
Jan 9, 2019
65e80d9
Merge branch 'fix-invitation-pending-for-existing-users' into verify-…
Jan 9, 2019
92a4b92
treat admin's e-mail address as verified
Jan 9, 2019
833c813
Merge branch 'master' into verify-admin-email-address
Jan 9, 2019
20ed45c
add verification endpoint
Jan 10, 2019
5bf4ac0
send verification e-mail
Jan 10, 2019
91ad2b7
Update client/app/components/empty-state/empty-state.html
jezdez Jan 10, 2019
19ce71c
Update redash/authentication/account.py
jezdez Jan 10, 2019
a1163a1
Update redash/handlers/authentication.py
jezdez Jan 10, 2019
05dbc60
Update redash/templates/emails/verify.html
jezdez Jan 10, 2019
1dca1ab
Update redash/authentication/account.py
jezdez Jan 10, 2019
733ba60
Update redash/templates/verify.html
jezdez Jan 10, 2019
4b6bc70
Update redash/templates/emails/verify.txt
jezdez Jan 13, 2019
9a0a925
add link in case redirects are disabled
Jan 13, 2019
3b175e2
Merge branch 'verify-admin-email-address' of github.com:getredash/red…
Jan 13, 2019
fb0137a
POSTing to /email_verification makes more sense than GETting /send_ve…
Jan 13, 2019
0f43e89
Merge branch 'master' into verify-admin-email-address
Jan 13, 2019
9574fd6
avoid sending invitations when no_invite is passed along
Jan 13, 2019
7b96d95
Update client/app/pages/users/new.html
arikfr Jan 13, 2019
42f48fd
move e-mail verification prompt to home-page
Jan 13, 2019
ac658a8
Merge branch 'verify-admin-email-address' of github.com:getredash/red…
Jan 13, 2019
c7f80ef
get rid of redundant $scope
Jan 13, 2019
c1999ba
return JSON
Jan 13, 2019
0412f65
Merge branch 'master' into verify-admin-email-address
Jan 13, 2019
27298da
flip is_email_verified's default value so that existing users do not
Jan 13, 2019
4ac9a25
Merge branch 'verify-admin-email-address' of github.com:getredash/red…
Jan 13, 2019
6e8f69b
e-mail verification propmt isn't dangerous, it just wants to warn you
Jan 13, 2019
3399059
Merge branch 'master' into verify-admin-email-address
Jan 13, 2019
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
3 changes: 2 additions & 1 deletion client/app/components/empty-state/empty-state.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<div ng-if="!$ctrl.isEmailVerified" class="alert alert-danger">We have sent an email with a confirmation link to your email address. Please follow the link to verify your email address. <a ng-click="verifyEmail()">Resend email</a>.</div>
rauchy marked this conversation as resolved.
Show resolved Hide resolved
<div class="empty-state bg-white tiled" ng-if="$ctrl.shouldShowOnboarding()">
<div class="empty-state__summary">
<h4 ng-if="$ctrl.title">{{$ctrl.title}}</h4>
Expand Down Expand Up @@ -32,4 +33,4 @@ <h4>Let's get started</h4>
</a>
</p>
</div>
</div>
</div>
9 changes: 8 additions & 1 deletion client/app/components/empty-state/empty-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ const EmptyStateComponent = {
showInviteStep: '<',
onboardingMode: '<',
},
controller($uibModal, OrganizationStatus, currentUser) {
controller($scope, $http, $uibModal, OrganizationStatus, currentUser, toastr) {
this.isAdmin = currentUser.isAdmin;
this.isEmailVerified = currentUser.is_email_verified;

this.dataSourceStepCompleted = OrganizationStatus.objectCounters.data_sources > 0;
this.queryStepCompleted = OrganizationStatus.objectCounters.queries > 0;
Expand Down Expand Up @@ -45,6 +46,12 @@ const EmptyStateComponent = {
},
});
};

$scope.verifyEmail = () => {
rauchy marked this conversation as resolved.
Show resolved Hide resolved
$http.get('/send_verification').success((data) => {
toastr.success(data);
});
};
},
};

Expand Down
2 changes: 0 additions & 2 deletions client/app/pages/users/new.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ <h3>New User</h3>

<div ng-if="user.created" class="alert alert-success alert-invited">
<h4>The user has been created and should receive an invite email soon</h4>
rauchy marked this conversation as resolved.
Show resolved Hide resolved
<p>You can use the following link to invite them yourself:</p>
<textarea class="form-control m-t-10" rows="2" readonly>{{ inviteLink }}</textarea>
</div>
</form>
</div>
Expand Down
3 changes: 0 additions & 3 deletions client/app/pages/users/new.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { absoluteUrl } from '@/services/utils';
import template from './new.html';

function NewUserCtrl($scope, toastr, currentUser, Events, User) {
Events.record('view', 'page', 'users/new');

$scope.inviteLink = '';

$scope.user = new User({});
$scope.saveUser = () => {
Expand All @@ -15,7 +13,6 @@ function NewUserCtrl($scope, toastr, currentUser, Events, User) {
$scope.user.$save((user) => {
$scope.user = user;
$scope.user.created = true;
$scope.inviteLink = absoluteUrl(user.invite_link);
toastr.success('Saved.');
}, (error) => {
const message = error.data.message || 'Failed saving.';
Expand Down
6 changes: 2 additions & 4 deletions client/app/pages/users/show.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,8 @@ function UserCtrl(
};

$scope.resendInvitation = () => {
$http.post(`api/users/${$scope.user.id}/invite`).success((data) => {
const inviteLink = absoluteUrl(data.invite_link);
toastr.success(`You can use the following link to invite them yourself: <textarea class="form-control m-t-10" rows="3" readonly>${inviteLink}</textarea>`, 'Invitation sent.', {
allowHtml: true,
$http.post(`api/users/${$scope.user.id}/invite`).success(() => {
toastr.success('Invitation sent.', {
timeOut: 10000,
});
});
Expand Down
3 changes: 2 additions & 1 deletion redash/authentication/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ def create_and_login_user(org, name, email, picture=None):
return None
if user_object.is_invitation_pending:
user_object.is_invitation_pending = False
user_object.is_email_verified = True
models.db.session.commit()
if user_object.name != name:
logger.debug("Updating user name (%r -> %r)", user_object.name, name)
Expand All @@ -269,7 +270,7 @@ def create_and_login_user(org, name, email, picture=None):
except NoResultFound:
logger.debug("Creating user object (%r)", name)
user_object = models.User(org=org, name=name, email=email, is_invitation_pending=False,
_profile_image_url=picture, group_ids=[org.default_group.id])
is_email_verified=True, _profile_image_url=picture, group_ids=[org.default_group.id])
models.db.session.add(user_object)
models.db.session.commit()

Expand Down
19 changes: 19 additions & 0 deletions redash/authentication/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ def invite_token(user):
return serializer.dumps(str(user.id))


def verify_link_for_user(user):
token = invite_token(user)
verify_url = "{}/verify/{}".format(base_url(user.org), token)

return verify_url


def invite_link_for_user(user):
token = invite_token(user)
invite_url = "{}/invite/{}".format(base_url(user.org), token)
Expand All @@ -35,6 +42,18 @@ def validate_token(token):
return serializer.loads(token, max_age=max_token_age)


def send_verify_email(user, org):
context = {
'user': user,
'verify_url': verify_link_for_user(user),
}
html_content = render_template('emails/verify.html', **context)
text_content = render_template('emails/verify.txt', **context)
subject = u"{}, please verify your email address".format(user.name)

send_mail.delay([user.email], subject, html_content, text_content)


def send_invite_email(inviter, invited, invite_url, org):
context = dict(inviter=inviter, invited=invited, org=org, invite_url=invite_url)
html_content = render_template('emails/invite.html', **context)
Expand Down
31 changes: 30 additions & 1 deletion redash/handlers/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from redash.authentication import current_org, get_login_url, get_next_path
from redash.authentication.account import (BadSignature, SignatureExpired,
send_password_reset_email,
send_verify_email,
validate_token)
from redash.handlers import routes
from redash.handlers.base import json_response, org_scoped_rule
Expand Down Expand Up @@ -55,6 +56,7 @@ def render_token_login_page(template, org_slug, token):
status_code = 400
else:
user.is_invitation_pending = False
user.is_email_verified = True
user.hash_password(request.form['password'])
models.db.session.add(user)
login_user(user)
Expand Down Expand Up @@ -83,6 +85,24 @@ def reset(token, org_slug=None):
return render_token_login_page("reset.html", org_slug, token)


@routes.route(org_scoped_rule('/verify/<token>'), methods=['GET'])
def verify(token, org_slug=None):
try:
user_id = validate_token(token)
org = current_org._get_current_object()
user = models.User.get_by_id_and_org(user_id, org)
except (BadSignature, NoResultFound):
logger.exception("Failed to verify email verification token: %s, org=%s", token, org_slug)
return render_template("error.html",
error_message="Your verification link is invalid. Please ask for a new one."), 400

user.is_email_verified = True
models.db.session.add(user)
models.db.session.commit()

return render_template("verify.html", org_slug=org_slug)


@routes.route(org_scoped_rule('/forgot'), methods=['GET', 'POST'])
def forgot_password(org_slug=None):
if not current_org.get_setting('auth_password_login_enabled'):
Expand All @@ -102,6 +122,14 @@ def forgot_password(org_slug=None):
return render_template("forgot.html", submitted=submitted)


@routes.route(org_scoped_rule('/send_verification'))
rauchy marked this conversation as resolved.
Show resolved Hide resolved
def send_verification(org_slug=None):
if not current_user.is_email_verified:
send_verify_email(current_user, current_org)

return "Please check your email inbox in order to verify your email address.", 200
rauchy marked this conversation as resolved.
Show resolved Hide resolved


@routes.route(org_scoped_rule('/login'), methods=['GET', 'POST'])
@limiter.limit(settings.THROTTLE_LOGIN_PATTERN)
def login(org_slug=None):
Expand Down Expand Up @@ -224,7 +252,8 @@ def session(org_slug=None):
'name': current_user.name,
'email': current_user.email,
'groups': current_user.group_ids,
'permissions': current_user.permissions
'permissions': current_user.permissions,
'is_email_verified': current_user.is_email_verified
}

return json_response({
Expand Down
2 changes: 1 addition & 1 deletion redash/handlers/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def create_org(org_name, user_name, email, password):
user = User(org=default_org,
name=user_name,
email=email,
is_invitation_pending=False,
is_email_verified=True,
group_ids=[admin_group.id, default_group.id])
user.hash_password(password)

Expand Down
16 changes: 3 additions & 13 deletions redash/handlers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
def invite_user(org, inviter, user):
invite_url = invite_link_for_user(user)
send_invite_email(inviter, user, invite_url, org)
return invite_url


class UserListResource(BaseResource):
Expand Down Expand Up @@ -119,15 +118,9 @@ def post(self):
'object_type': 'user'
})

if request.args.get('no_invite') is not None:
rauchy marked this conversation as resolved.
Show resolved Hide resolved
invite_url = invite_link_for_user(user)
else:
invite_url = invite_user(self.current_org, self.current_user, user)

d = user.to_dict()
d['invite_link'] = invite_url
invite_user(self.current_org, self.current_user, user)

return d
return user.to_dict()


class UserInviteResource(BaseResource):
Expand All @@ -136,10 +129,7 @@ def post(self, user_id):
user = models.User.get_by_id_and_org(user_id, self.current_org)
invite_url = invite_user(self.current_org, self.current_user, user)

d = user.to_dict()
d['invite_link'] = invite_url

return d
return user.to_dict()


class UserResetPasswordResource(BaseResource):
Expand Down
2 changes: 2 additions & 0 deletions redash/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class User(TimestampMixin, db.Model, BelongsToOrgMixin, UserMixin, PermissionsCh
active_at = json_cast_property(db.DateTime(True), 'details', 'active_at',
default=None)
is_invitation_pending = json_cast_property(db.Boolean(True), 'details', 'is_invitation_pending', default=False)
is_email_verified = json_cast_property(db.Boolean(True), 'details', 'is_email_verified', default=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

woot!


__tablename__ = 'users'
__table_args__ = (
Expand Down Expand Up @@ -140,6 +141,7 @@ def to_dict(self, with_api_key=False):
'is_disabled': self.is_disabled,
'active_at': self.active_at,
'is_invitation_pending': self.is_invitation_pending,
'is_email_verified': self.is_email_verified,
}

if self.password_hash is None:
Expand Down
15 changes: 15 additions & 0 deletions redash/templates/emails/verify.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% extends "emails/layout.html" %}

{% block content %}

<p class="intercom-align-left" style="line-height: 1.5; margin: 0 0 17px; text-align: left !important" align="left">Hi {{ user.name }},</p>
<h2 class="intercom-align-left" style="color: #282F33; font-size: 18px; font-weight: bold; margin-bottom: 7px; margin-top: 30px; text-align: left !important" align="left">
Please verify that {{ user.email }} is your correct email address by visiting the following link:
</h2>
<table class="intercom-container intercom-align-center" align="center" style="border-collapse: collapse; border-spacing: 0; margin: 17px auto; table-layout: fixed; text-align: center !important"><tr><td style="background: #0071b2; border: 1px none #dadada; border-radius: 3px; font-family: Helvetica, Arial, sans-serif; font-size: 16px; margin: 0; padding: 12px 35px; text-align: left; vertical-align: top" align="left" bgcolor="#0071b2" valign="top"><a class="intercom-h2b-button" target="_blank"
href="{{ verify_url }}" style="background: #0071b2; border: none; border-radius: 3px; color: white; display: inline-block; font-size: 14px; font-weight: bold; outline: none !important; text-decoration: none">Verify Address</a></td></tr></table>
<p class="intercom-align-left" style="line-height: 1.5; margin: 0 0 17px; text-align: left !important" align="left">
<small>You may copy/paste this link into your browser: {{ verify_url }}</small>
</p>

{% endblock %}
7 changes: 7 additions & 0 deletions redash/templates/emails/verify.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Hi {{ user.name }},

Please verify that {{ user.email }} is your correct email address by visiting the following link:

{{ verify_url }}

Thank you.
1 change: 1 addition & 0 deletions redash/templates/layouts/signed_out.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
{% block head %}{% endblock %}
</head>
<body class="d-flex flex-column justify-content-center align-items-center signed-out">

Expand Down
14 changes: 14 additions & 0 deletions redash/templates/verify.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% extends "layouts/signed_out.html" %}
{% block title %}E-mail Verified{% endblock %}
{% block head %}
<meta http-equiv="refresh" content="5;URL='{{ url_for('redash.index') }}'" />
{% endblock %}
{% block content %}
<div class="fixed-width-page">
<div class="bg-white tiled">
<div>
Thanks for verifying your email address. You will be redirected back to the app within a few seconds. <a href="{{ url_for('redash.index') }}">Click here</a> if you are not redirected.
</div>
rauchy marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
{% endblock %}