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