diff --git a/data/model/organization.py b/data/model/organization.py index a855385e9..e83c6907e 100644 --- a/data/model/organization.py +++ b/data/model/organization.py @@ -72,6 +72,9 @@ def __get_org_admin_users(org): .where(Team.organization == org, TeamRole.name == 'admin', User.robot == False) .distinct()) +def get_admin_users(org): + """ Returns the owner users for the organization. """ + return __get_org_admin_users(org) def remove_organization_member(org, user_obj): org_admins = [u.username for u in __get_org_admin_users(org)] diff --git a/data/model/user.py b/data/model/user.py index d05ea1693..9ab763071 100644 --- a/data/model/user.py +++ b/data/model/user.py @@ -402,7 +402,7 @@ def create_reset_password_email_code(email): try: user = User.get(User.email == email) except User.DoesNotExist: - raise InvalidEmailAddressException('Email address was not found.'); + raise InvalidEmailAddressException('Email address was not found.') if user.organization: raise InvalidEmailAddressException('Organizations can not have passwords.') diff --git a/emails/orgrecovery.html b/emails/orgrecovery.html new file mode 100644 index 000000000..cda1d17e0 --- /dev/null +++ b/emails/orgrecovery.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block content %} + +

Organization {{ organization }} recovery

+ +A user at {{ app_link() }} has attempted to recover organization {{ organization | user_reference }} via this email address. +
+
+Please login with one of the following user accounts to access this organization: + +
+If you did not make this request, your organization has not been compromised and the user was +not given access. Please disregard this email. + +{% endblock %} diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 1801598f7..6d45c8e7a 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -27,7 +27,7 @@ from auth.permissions import (AdministerOrganizationPermission, CreateRepository from auth.auth_context import get_authenticated_user from auth import scopes from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email, - send_password_changed) + send_password_changed, send_org_recovery_email) from util.names import parse_single_urn @@ -647,10 +647,35 @@ class Recovery(ApiResource): @validate_json_request('RequestRecovery') def post(self): """ Request a password recovery email.""" + def redact(value): + threshold = max((len(value) / 3) - 1, 1) + v = '' + for i in range(0, len(value)): + if i < threshold or i >= len(value) - threshold: + v = v + value[i] + else: + v = v + u'\u2022' + + return v + email = request.get_json()['email'] + user = model.user.find_user_by_email(email) + if not user: + raise model.InvalidEmailAddressException('Email address was not found.') + + if user.organization: + send_org_recovery_email(user, model.organization.get_admin_users(user)) + return { + 'status': 'org', + 'orgemail': email, + 'orgname': redact(user.username), + } + code = model.user.create_reset_password_email_code(email) send_recovery_email(email, code.code) - return 'Created', 201 + return { + 'status': 'sent', + } @resource('/v1/user/notifications') diff --git a/static/directives/user-setup.html b/static/directives/user-setup.html index 291614fa9..7a5aac604 100644 --- a/static/directives/user-setup.html +++ b/static/directives/user-setup.html @@ -39,16 +39,24 @@
-
+
+
+
- -
{{errorMessage}}
- -
Account recovery email was sent.
+
{{errorMessage}}
+
+ The e-mail address {{ sent.orgemail }} is assigned to organization {{ sent.orgname }}. + To access that organization, an admin user must be used. +

+ An e-mail has been sent to + {{ sent.orgemail }} with the full list of admin users. +
+
Account recovery email was sent.
diff --git a/static/js/directives/ui/user-setup.js b/static/js/directives/ui/user-setup.js index de639ca98..70782e56d 100644 --- a/static/js/directives/ui/user-setup.js +++ b/static/js/directives/ui/user-setup.js @@ -24,15 +24,15 @@ angular.module('quay').directive('userSetup', function () { $scope.sendRecovery = function() { $scope.sendingRecovery = true; - ApiService.requestRecoveryEmail($scope.recovery).then(function() { + ApiService.requestRecoveryEmail($scope.recovery).then(function(resp) { $scope.invalidRecovery = false; $scope.errorMessage = ''; - $scope.sent = true; + $scope.sent = resp; $scope.sendingRecovery = false; }, function(resp) { $scope.invalidRecovery = true; $scope.errorMessage = ApiService.getErrorMessage(resp, 'Cannot send recovery email'); - $scope.sent = false; + $scope.sent = null; $scope.sendingRecovery = false; }); }; diff --git a/util/useremails.py b/util/useremails.py index f7b319a13..7320c4155 100644 --- a/util/useremails.py +++ b/util/useremails.py @@ -117,6 +117,15 @@ def send_repo_authorization_email(namespace, repository, email, token): 'token': token }, action=action) + +def send_org_recovery_email(org, admin_users): + subject = 'Organization %s recovery' % (org.username) + send_email(org.email, subject, 'orgrecovery', { + 'organization': org.username, + 'admin_usernames': [user.username for user in admin_users], + }) + + def send_recovery_email(email, token): action = GmailAction.view('Recover Account', 'recovery?code=' + token, 'Recovery of an account')