Add a feature flag for disabling all emails
This commit is contained in:
parent
dc685b2387
commit
f3b03ebc34
10 changed files with 71 additions and 32 deletions
|
@ -162,6 +162,9 @@ class DefaultConfig(object):
|
|||
# Feature Flag: Dockerfile build support.
|
||||
FEATURE_BUILD_SUPPORT = True
|
||||
|
||||
# Feature Flag: Whether emails are enabled.
|
||||
FEATURE_MAILING = True
|
||||
|
||||
DISTRIBUTED_STORAGE_CONFIG = {
|
||||
'local_eu': ['LocalStorage', {'storage_path': 'test/data/registry/eu'}],
|
||||
'local_us': ['LocalStorage', {'storage_path': 'test/data/registry/us'}],
|
||||
|
|
|
@ -93,7 +93,7 @@ def hash_password(password, salt=None):
|
|||
def is_create_user_allowed():
|
||||
return True
|
||||
|
||||
def create_user(username, password, email):
|
||||
def create_user(username, password, email, auto_verify=False):
|
||||
""" Creates a regular user, if allowed. """
|
||||
if not validate_password(password):
|
||||
raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE)
|
||||
|
@ -102,11 +102,8 @@ def create_user(username, password, email):
|
|||
raise TooManyUsersException()
|
||||
|
||||
created = _create_user(username, email)
|
||||
|
||||
# Store the password hash
|
||||
pw_hash = hash_password(password)
|
||||
created.password_hash = pw_hash
|
||||
|
||||
created.password_hash = hash_password(password)
|
||||
created.verified = auto_verify
|
||||
created.save()
|
||||
|
||||
return created
|
||||
|
@ -343,12 +340,11 @@ def remove_team(org_name, team_name, removed_by_username):
|
|||
team.delete_instance(recursive=True, delete_nullable=True)
|
||||
|
||||
|
||||
def add_or_invite_to_team(inviter, team, user=None, email=None):
|
||||
def add_or_invite_to_team(inviter, team, user=None, email=None, requires_invite=True):
|
||||
# If the user is a member of the organization, then we simply add the
|
||||
# user directly to the team. Otherwise, an invite is created for the user/email.
|
||||
# We return None if the user was directly added and the invite object if the user was invited.
|
||||
requires_invite = True
|
||||
if user:
|
||||
if user and requires_invite:
|
||||
orgname = team.organization.username
|
||||
|
||||
# If the user is part of the organization (or a robot), then no invite is required.
|
||||
|
@ -833,9 +829,9 @@ def change_invoice_email(user, invoice_email):
|
|||
user.save()
|
||||
|
||||
|
||||
def update_email(user, new_email):
|
||||
def update_email(user, new_email, auto_verify=False):
|
||||
user.email = new_email
|
||||
user.verified = False
|
||||
user.verified = auto_verify
|
||||
user.save()
|
||||
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@ import logging
|
|||
from flask import request, abort
|
||||
|
||||
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
||||
log_action, validate_json_request, NotFound, internal_only)
|
||||
log_action, validate_json_request, NotFound, internal_only,
|
||||
show_if)
|
||||
|
||||
from app import tf
|
||||
from data import model
|
||||
|
@ -25,6 +26,7 @@ def record_view(record):
|
|||
|
||||
|
||||
@internal_only
|
||||
@show_if(features.MAILING)
|
||||
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
|
||||
class RepositoryAuthorizedEmail(RepositoryParamResource):
|
||||
""" Resource for checking and authorizing e-mail addresses to receive repo notifications. """
|
||||
|
|
|
@ -2,7 +2,7 @@ from flask import request
|
|||
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||
log_action, Unauthorized, NotFound, internal_only, require_scope,
|
||||
query_param, truthy_bool, parse_args, require_user_admin)
|
||||
query_param, truthy_bool, parse_args, require_user_admin, show_if)
|
||||
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
|
@ -10,6 +10,8 @@ from data import model
|
|||
from util.useremails import send_org_invite_email
|
||||
from util.gravatar import compute_hash
|
||||
|
||||
import features
|
||||
|
||||
def try_accept_invite(code, user):
|
||||
(team, inviter) = model.confirm_team_invite(code, user)
|
||||
|
||||
|
@ -26,7 +28,8 @@ def try_accept_invite(code, user):
|
|||
|
||||
|
||||
def handle_addinvite_team(inviter, team, user=None, email=None):
|
||||
invite = model.add_or_invite_to_team(inviter, team, user, email)
|
||||
invite = model.add_or_invite_to_team(inviter, team, user, email,
|
||||
requires_invite = features.MAILING)
|
||||
if not invite:
|
||||
# User was added to the team directly.
|
||||
return
|
||||
|
@ -275,6 +278,7 @@ class TeamMember(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/organization/<orgname>/team/<teamname>/invite/<email>')
|
||||
@show_if(features.MAILING)
|
||||
class InviteTeamMember(ApiResource):
|
||||
""" Resource for inviting a team member via email address. """
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
|
@ -331,6 +335,7 @@ class InviteTeamMember(ApiResource):
|
|||
|
||||
@resource('/v1/teaminvite/<code>')
|
||||
@internal_only
|
||||
@show_if(features.MAILING)
|
||||
class TeamMemberInvite(ApiResource):
|
||||
""" Resource for managing invites to jon a team. """
|
||||
@require_user_admin
|
||||
|
|
|
@ -168,6 +168,8 @@ class User(ApiResource):
|
|||
logger.debug('Changing password for user: %s', user.username)
|
||||
log_action('account_change_password', user.username)
|
||||
model.change_password(user, user_data['password'])
|
||||
|
||||
if features.MAILING:
|
||||
send_password_changed(user.username, user.email)
|
||||
|
||||
if 'invoice_email' in user_data:
|
||||
|
@ -180,10 +182,13 @@ class User(ApiResource):
|
|||
# Email already used.
|
||||
raise request_error(message='E-mail address already used')
|
||||
|
||||
if features.MAILING:
|
||||
logger.debug('Sending email to change email address for user: %s',
|
||||
user.username)
|
||||
code = model.create_confirm_email_code(user, new_email=new_email)
|
||||
send_change_email(user.username, user_data['email'], code.code)
|
||||
else:
|
||||
model.update_email(user, new_email, auto_verify=not features.MAILING)
|
||||
|
||||
except model.InvalidPasswordException, ex:
|
||||
raise request_error(exception=ex)
|
||||
|
@ -207,9 +212,7 @@ class User(ApiResource):
|
|||
|
||||
try:
|
||||
new_user = model.create_user(user_data['username'], user_data['password'],
|
||||
user_data['email'])
|
||||
code = model.create_confirm_email_code(new_user)
|
||||
send_confirmation_email(new_user.username, new_user.email, code.code)
|
||||
user_data['email'], auto_verify=not features.MAILING)
|
||||
|
||||
# Handle any invite codes.
|
||||
parsed_invite = parse_single_urn(invite_code)
|
||||
|
@ -221,7 +224,17 @@ class User(ApiResource):
|
|||
except model.DataModelException:
|
||||
pass
|
||||
|
||||
return 'Created', 201
|
||||
|
||||
if features.MAILING:
|
||||
code = model.create_confirm_email_code(new_user)
|
||||
send_confirmation_email(new_user.username, new_user.email, code.code)
|
||||
return {
|
||||
'awaiting_verification': True
|
||||
}
|
||||
else:
|
||||
common_login(new_user)
|
||||
return user_view(new_user)
|
||||
|
||||
except model.TooManyUsersException as ex:
|
||||
raise license_error(exception=ex)
|
||||
except model.DataModelException as ex:
|
||||
|
@ -441,6 +454,7 @@ class DetachExternal(ApiResource):
|
|||
|
||||
|
||||
@resource("/v1/recovery")
|
||||
@show_if(features.MAILING)
|
||||
@internal_only
|
||||
class Recovery(ApiResource):
|
||||
""" Resource for requesting a password recovery email. """
|
||||
|
|
|
@ -223,6 +223,7 @@ def receipt():
|
|||
|
||||
|
||||
@web.route('/authrepoemail', methods=['GET'])
|
||||
@route_show_if(features.MAILING)
|
||||
def confirm_repo_email():
|
||||
code = request.values['code']
|
||||
record = None
|
||||
|
@ -243,6 +244,7 @@ def confirm_repo_email():
|
|||
|
||||
|
||||
@web.route('/confirm', methods=['GET'])
|
||||
@route_show_if(features.MAILING)
|
||||
def confirm_email():
|
||||
code = request.values['code']
|
||||
user = None
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel panel-default" quay-show="Features.MAILING">
|
||||
<div class="panel-heading">
|
||||
<h6 class="panel-title accordion-title">
|
||||
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseForgot">
|
||||
|
@ -37,7 +37,8 @@
|
|||
</h6>
|
||||
</div>
|
||||
<div id="collapseForgot" class="panel-collapse collapse out">
|
||||
<div class="panel-body">
|
||||
<div class="quay-spinner" ng-show="sendingRecovery"></div>
|
||||
<div class="panel-body" ng-show="!sendingRecovery">
|
||||
<form class="form-signin" ng-submit="sendRecovery();">
|
||||
<input type="text" class="form-control input-lg" placeholder="Email" ng-model="recovery.email">
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Send Recovery Email</button>
|
||||
|
|
|
@ -1273,7 +1273,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
return userService;
|
||||
}]);
|
||||
|
||||
$provide.factory('ExternalNotificationData', ['Config', function(Config) {
|
||||
$provide.factory('ExternalNotificationData', ['Config', 'Features', function(Config, Features) {
|
||||
var externalNotificationData = {};
|
||||
|
||||
var events = [
|
||||
|
@ -1327,7 +1327,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
'type': 'email',
|
||||
'title': 'E-mail address'
|
||||
}
|
||||
]
|
||||
],
|
||||
'enabled': Features.MAILING
|
||||
},
|
||||
{
|
||||
'id': 'webhook',
|
||||
|
@ -1407,7 +1408,13 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
};
|
||||
|
||||
externalNotificationData.getSupportedMethods = function() {
|
||||
return methods;
|
||||
var filtered = [];
|
||||
for (var i = 0; i < methods.length; ++i) {
|
||||
if (methods[i].enabled !== false) {
|
||||
filtered.push(methods[i]);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
};
|
||||
|
||||
externalNotificationData.getEventInfo = function(event) {
|
||||
|
@ -2598,14 +2605,18 @@ quayApp.directive('userSetup', function () {
|
|||
},
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
|
||||
$scope.sendRecovery = function() {
|
||||
$scope.sendingRecovery = true;
|
||||
|
||||
ApiService.requestRecoveryEmail($scope.recovery).then(function() {
|
||||
$scope.invalidRecovery = false;
|
||||
$scope.errorMessage = '';
|
||||
$scope.sent = true;
|
||||
$scope.sendingRecovery = false;
|
||||
}, function(result) {
|
||||
$scope.invalidRecovery = true;
|
||||
$scope.errorMessage = result.data;
|
||||
$scope.sent = false;
|
||||
$scope.sendingRecovery = false;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -2783,15 +2794,19 @@ quayApp.directive('signupForm', function () {
|
|||
$scope.newUser['inviteCode'] = $scope.inviteCode;
|
||||
}
|
||||
|
||||
ApiService.createNewUser($scope.newUser).then(function() {
|
||||
ApiService.createNewUser($scope.newUser).then(function(resp) {
|
||||
$scope.registering = false;
|
||||
$scope.awaitingConfirmation = true;
|
||||
$scope.awaitingConfirmation = !!resp['awaiting_verification'];
|
||||
|
||||
if (Config.MIXPANEL_KEY) {
|
||||
mixpanel.alias($scope.newUser.username);
|
||||
}
|
||||
|
||||
$scope.userRegistered({'username': $scope.newUser.username});
|
||||
|
||||
if (!$scope.awaitingConfirmation) {
|
||||
document.location = '/';
|
||||
}
|
||||
}, function(result) {
|
||||
$scope.registering = false;
|
||||
UIService.showFormError('#signupButton', result);
|
||||
|
|
|
@ -122,7 +122,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" ng-show="!updatingUser" >
|
||||
<div class="panel" ng-show="!updatingUser" quay-show="Features.MAILING">
|
||||
<div class="panel-title">Change e-mail address</div>
|
||||
|
||||
<div class="panel-body">
|
||||
|
|
|
@ -34,6 +34,7 @@ class TestConfig(DefaultConfig):
|
|||
|
||||
FEATURE_SUPER_USERS = True
|
||||
FEATURE_BILLING = True
|
||||
FEATURE_MAILING = True
|
||||
SUPER_USERS = ['devtable']
|
||||
|
||||
LICENSE_USER_LIMIT = 500
|
||||
|
|
Reference in a new issue