Add e-mail authorization to the repository notification flow. Also validates the creation of the other notification methods.
This commit is contained in:
parent
56fec63fcd
commit
34fc279092
15 changed files with 483 additions and 34 deletions
|
@ -382,10 +382,27 @@ class RepositoryNotification(BaseModel):
|
|||
config_json = TextField()
|
||||
|
||||
|
||||
class RepositoryAuthorizedEmail(BaseModel):
|
||||
repository = ForeignKeyField(Repository, index=True)
|
||||
email = CharField()
|
||||
code = CharField(default=random_string_generator(), unique=True, index=True)
|
||||
confirmed = BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
read_slaves = (read_slave,)
|
||||
indexes = (
|
||||
# create a unique index on email and repository
|
||||
(('email', 'repository'), True),
|
||||
)
|
||||
|
||||
|
||||
|
||||
all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility,
|
||||
RepositoryTag, EmailConfirmation, FederatedLogin, LoginService, QueueItem,
|
||||
RepositoryBuild, Team, TeamMember, TeamRole, LogEntryKind, LogEntry,
|
||||
PermissionPrototype, ImageStorage, BuildTriggerService, RepositoryBuildTrigger,
|
||||
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind,
|
||||
Notification, ImageStorageLocation, ImageStoragePlacement,
|
||||
ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification]
|
||||
ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification,
|
||||
RepositoryAuthorizedEmail]
|
||||
|
|
|
@ -1763,3 +1763,39 @@ def check_health():
|
|||
return found_count > 0
|
||||
except:
|
||||
return False
|
||||
|
||||
def get_email_authorized_for_repo(namespace, repository, email):
|
||||
found = list(RepositoryAuthorizedEmail.select()
|
||||
.join(Repository)
|
||||
.where(Repository.namespace == namespace,
|
||||
Repository.name == repository,
|
||||
RepositoryAuthorizedEmail.email == email)
|
||||
.switch(RepositoryAuthorizedEmail)
|
||||
.limit(1))
|
||||
if not found or len(found) < 1:
|
||||
return None
|
||||
|
||||
return found[0]
|
||||
|
||||
|
||||
def create_email_authorization_for_repo(namespace_name, repository_name, email):
|
||||
try:
|
||||
repo = Repository.get(Repository.name == repository_name,
|
||||
Repository.namespace == namespace_name)
|
||||
except Repository.DoesNotExist:
|
||||
raise DataModelException('Invalid repository %s/%s' %
|
||||
(namespace_name, repository_name))
|
||||
|
||||
return RepositoryAuthorizedEmail.create(repository=repo, email=email, confirmed=False)
|
||||
|
||||
|
||||
def confirm_email_authorization_for_repo(code):
|
||||
try:
|
||||
found = RepositoryAuthorizedEmail.get(RepositoryAuthorizedEmail.code == code)
|
||||
except RepositoryAuthorizedEmail.DoesNotExist:
|
||||
raise DataModelException('Invalid confirmation code.')
|
||||
|
||||
found.confirmed = True
|
||||
found.save()
|
||||
|
||||
return found
|
||||
|
|
|
@ -308,6 +308,7 @@ import endpoints.api.permission
|
|||
import endpoints.api.prototype
|
||||
import endpoints.api.repository
|
||||
import endpoints.api.repositorynotification
|
||||
import endpoints.api.repoemail
|
||||
import endpoints.api.repotoken
|
||||
import endpoints.api.robot
|
||||
import endpoints.api.search
|
||||
|
|
56
endpoints/api/repoemail.py
Normal file
56
endpoints/api/repoemail.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
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)
|
||||
|
||||
from app import tf
|
||||
from data import model
|
||||
from data.database import db
|
||||
from util.useremails import send_repo_authorization_email
|
||||
|
||||
import features
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def record_view(record):
|
||||
return {
|
||||
'email': record.email,
|
||||
'repository': record.repository.name,
|
||||
'namespace': record.repository.namespace,
|
||||
'confirmed': record.confirmed
|
||||
}
|
||||
|
||||
|
||||
@internal_only
|
||||
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
|
||||
class RepositoryAuthorizedEmail(RepositoryParamResource):
|
||||
""" Resource for checking and authorizing e-mail addresses to receive repo notifications. """
|
||||
@require_repo_admin
|
||||
@nickname('checkRepoEmailAuthorized')
|
||||
def get(self, namespace, repository, email):
|
||||
""" Checks to see if the given e-mail address is authorized on this repository. """
|
||||
record = model.get_email_authorized_for_repo(namespace, repository, email)
|
||||
if not record:
|
||||
abort(404)
|
||||
|
||||
return record_view(record)
|
||||
|
||||
|
||||
@require_repo_admin
|
||||
@nickname('sendAuthorizeRepoEmail')
|
||||
def post(self, namespace, repository, email):
|
||||
""" Starts the authorization process for an e-mail address on a repository. """
|
||||
|
||||
with tf(db):
|
||||
record = model.get_email_authorized_for_repo(namespace, repository, email)
|
||||
if record and record.confirmed:
|
||||
return record_view(record)
|
||||
|
||||
if not record:
|
||||
record = model.create_email_authorization_for_repo(namespace, repository, email)
|
||||
|
||||
send_repo_authorization_email(namespace, repository, email, record.code)
|
||||
return record_view(record)
|
|
@ -1,11 +1,13 @@
|
|||
import json
|
||||
|
||||
from flask import request
|
||||
from flask import request, abort
|
||||
|
||||
from app import notification_queue
|
||||
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
||||
log_action, validate_json_request, api, NotFound)
|
||||
log_action, validate_json_request, api, NotFound, request_error)
|
||||
from endpoints.notificationevent import NotificationEvent
|
||||
from endpoints.notificationmethod import (NotificationMethod,
|
||||
CannotValidateNotificationMethodException)
|
||||
from data import model
|
||||
|
||||
|
||||
|
@ -62,6 +64,15 @@ class RepositoryNotificationList(RepositoryParamResource):
|
|||
repo = model.get_repository(namespace, repository)
|
||||
json = request.get_json()
|
||||
|
||||
method_handler = NotificationMethod.get_method(json['method'])
|
||||
if not method_handler:
|
||||
raise request_error(message='Unknown method')
|
||||
|
||||
try:
|
||||
method_handler.validate(repo, json['config'])
|
||||
except CannotValidateNotificationMethodException as ex:
|
||||
raise request_error(message=ex.message)
|
||||
|
||||
notification = model.create_repo_notification(repo, json['event'], json['method'],
|
||||
json['config'])
|
||||
|
||||
|
|
|
@ -14,6 +14,10 @@ logger = logging.getLogger(__name__)
|
|||
class InvalidNotificationMethodException(Exception):
|
||||
pass
|
||||
|
||||
class CannotValidateNotificationMethodException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotificationMethod(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
@ -25,6 +29,13 @@ class NotificationMethod(object):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def validate(self, repository, config_data):
|
||||
"""
|
||||
Validates that the notification can be created with the given data. Throws
|
||||
a CannotValidateNotificationMethodException on failure.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def perform(self, notification, event_handler, notification_data):
|
||||
"""
|
||||
Performs the notification method.
|
||||
|
@ -49,36 +60,32 @@ class QuayNotificationMethod(NotificationMethod):
|
|||
def method_name(cls):
|
||||
return 'quay_notification'
|
||||
|
||||
def perform(self, notification, event_handler, notification_data):
|
||||
config_data = json.loads(notification.config_json)
|
||||
repository_id = notification_data['repository_id']
|
||||
repository = model.lookup_repository(repository_id)
|
||||
if not repository:
|
||||
# Probably deleted.
|
||||
return True
|
||||
|
||||
# Lookup the target user or team to which we'll send the notification.
|
||||
def validate(self, repository, config_data):
|
||||
status, err_message, target_users = self.find_targets(repository, config_data)
|
||||
if err_message:
|
||||
raise CannotValidateNotificationMethodException(err_message)
|
||||
|
||||
def find_targets(self, repository, config_data):
|
||||
target_info = config_data['target']
|
||||
target_users = []
|
||||
|
||||
if target_info['kind'] == 'user':
|
||||
target = model.get_user(target_info['name'])
|
||||
if not target:
|
||||
# Just to be safe.
|
||||
return True
|
||||
return (True, 'Unknown user %s' % target_info['name'], [])
|
||||
|
||||
target_users.append(target)
|
||||
return (True, None, [target])
|
||||
elif target_info['kind'] == 'org':
|
||||
target = model.get_organization(target_info['name'])
|
||||
if not target:
|
||||
# Just to be safe.
|
||||
return True
|
||||
return (True, 'Unknown organization %s' % target_info['name'], None)
|
||||
|
||||
# Only repositories under the organization can cause notifications to that org.
|
||||
if target_info['name'] != repository.namespace:
|
||||
return False
|
||||
return (False, 'Organization name must match repository namespace')
|
||||
|
||||
target_users.append(target)
|
||||
return (True, None, [target])
|
||||
elif target_info['kind'] == 'team':
|
||||
# Lookup the team.
|
||||
team = None
|
||||
|
@ -86,13 +93,27 @@ class QuayNotificationMethod(NotificationMethod):
|
|||
team = model.get_organization_team(repository.namespace, target_info['name'])
|
||||
except model.InvalidTeamException:
|
||||
# Probably deleted.
|
||||
return True
|
||||
return (True, 'Unknown team %s' % target_info['name'], None)
|
||||
|
||||
# Lookup the team's members
|
||||
target_users = model.get_organization_team_members(team.id)
|
||||
return (True, None, model.get_organization_team_members(team.id))
|
||||
|
||||
|
||||
def perform(self, notification, event_handler, notification_data):
|
||||
repository_id = notification_data['repository_id']
|
||||
repository = model.lookup_repository(repository_id)
|
||||
if not repository:
|
||||
# Probably deleted.
|
||||
return True
|
||||
|
||||
# Lookup the target user or team to which we'll send the notification.
|
||||
config_data = json.loads(notification.config_json)
|
||||
status, err_message, target_users = self.find_targets(repository, config_data)
|
||||
if not status:
|
||||
return False
|
||||
|
||||
# For each of the target users, create a notification.
|
||||
for target_user in set(target_users):
|
||||
for target_user in set(target_users or []):
|
||||
model.create_notification(event_handler.event_name(), target_user,
|
||||
metadata=notification_data['event_data'])
|
||||
return True
|
||||
|
@ -103,6 +124,18 @@ class EmailMethod(NotificationMethod):
|
|||
def method_name(cls):
|
||||
return 'email'
|
||||
|
||||
def validate(self, repository, config_data):
|
||||
email = config_data.get('email', '')
|
||||
if not email:
|
||||
raise CannotValidateNotificationMethodException('Missing e-mail address')
|
||||
|
||||
record = model.get_email_authorized_for_repo(repository.namespace, repository.name, email)
|
||||
if not record or not record.confirmed:
|
||||
raise CannotValidateNotificationMethodException('The specified e-mail address '
|
||||
'is not authorized to receive '
|
||||
'notifications for this repository')
|
||||
|
||||
|
||||
def perform(self, notification, event_handler, notification_data):
|
||||
config_data = json.loads(notification.config_json)
|
||||
email = config_data.get('email', '')
|
||||
|
@ -129,6 +162,11 @@ class WebhookMethod(NotificationMethod):
|
|||
def method_name(cls):
|
||||
return 'webhook'
|
||||
|
||||
def validate(self, repository, config_data):
|
||||
url = config_data.get('url', '')
|
||||
if not url:
|
||||
raise CannotValidateNotificationMethodException('Missing webhook URL')
|
||||
|
||||
def perform(self, notification, event_handler, notification_data):
|
||||
config_data = json.loads(notification.config_json)
|
||||
url = config_data.get('url', '')
|
||||
|
|
|
@ -214,6 +214,26 @@ def receipt():
|
|||
abort(404)
|
||||
|
||||
|
||||
@web.route('/authrepoemail', methods=['GET'])
|
||||
def confirm_repo_email():
|
||||
code = request.values['code']
|
||||
record = None
|
||||
|
||||
try:
|
||||
record = model.confirm_email_authorization_for_repo(code)
|
||||
except model.DataModelException as ex:
|
||||
return render_page_template('confirmerror.html', error_message=ex.message)
|
||||
|
||||
message = """
|
||||
Your E-mail address has been authorized to receive notifications for repository
|
||||
<a href="%s://%s/repository/%s/%s">%s/%s</a>.
|
||||
""" % (app.config['PREFERRED_URL_SCHEME'], app.config['SERVER_HOSTNAME'],
|
||||
record.repository.namespace, record.repository.name,
|
||||
record.repository.namespace, record.repository.name)
|
||||
|
||||
return render_page_template('message.html', message=message)
|
||||
|
||||
|
||||
@web.route('/confirm', methods=['GET'])
|
||||
def confirm_email():
|
||||
code = request.values['code']
|
||||
|
|
|
@ -424,6 +424,12 @@ def populate_database():
|
|||
'build_subdir': '',
|
||||
}
|
||||
|
||||
record = model.create_email_authorization_for_repo(new_user_1.username, 'simple', 'jschorr@devtable.com')
|
||||
record.confirmed = True
|
||||
record.save()
|
||||
|
||||
model.create_email_authorization_for_repo(new_user_1.username, 'simple', 'jschorr+other@devtable.com')
|
||||
|
||||
build2 = model.create_repository_build(building, token, job_config,
|
||||
'68daeebd-a5b9-457f-80a0-4363b882f8ea',
|
||||
'build-name', trigger)
|
||||
|
|
|
@ -10,9 +10,26 @@
|
|||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="quay-spinner" ng-show="creating"></div>
|
||||
<!-- Creating spinner -->
|
||||
<div class="quay-spinner" ng-show="status == 'creating' || status == 'authorizing-email'"></div>
|
||||
|
||||
<table style="width: 100%" ng-show="!creating">
|
||||
<!-- Authorize e-mail view -->
|
||||
<div ng-show="status == 'authorizing-email-sent'">
|
||||
An e-mail has been sent to <code>{{ currentConfig.email }}</code>. Please click the link contained
|
||||
in the e-mail.
|
||||
<br><br>
|
||||
Waiting... <span class="quay-spinner"></span>
|
||||
</div>
|
||||
|
||||
<!-- Authorize e-mail view -->
|
||||
<div ng-show="status == 'unauthorized-email'">
|
||||
The e-mail address <code>{{ currentConfig.email }}</code> has not been authorized to receive
|
||||
notifications from this repository. Please click "Send Authorization E-mail" below to start
|
||||
the authorization process.
|
||||
</div>
|
||||
|
||||
<!-- Create View -->
|
||||
<table style="width: 100%" ng-show="status == ''">
|
||||
<tr>
|
||||
<td style="width: 120px">When this occurs:</td>
|
||||
<td>
|
||||
|
@ -59,7 +76,9 @@
|
|||
<td>{{ field.title }}:</td>
|
||||
<td>
|
||||
<div ng-switch on="field.type">
|
||||
<input type="email" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="email" required>
|
||||
<span ng-switch-when="email">
|
||||
<input type="email" class="form-control" ng-model="currentConfig[field.name]" required>
|
||||
</span>
|
||||
<input type="url" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="url" required>
|
||||
<input type="text" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="string" required>
|
||||
<div class="entity-search" namespace="repository.namespace"
|
||||
|
@ -68,7 +87,7 @@
|
|||
ng-model="currentConfig[field.name]"
|
||||
allowed-entities="['user', 'team', 'org']"
|
||||
ng-switch-when="entity">
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
@ -86,8 +105,18 @@
|
|||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Auth e-mail button bar -->
|
||||
<div class="modal-footer" ng-if="status == 'unauthorized-email'">
|
||||
<button type="button" class="btn btn-success" ng-click="sendAuthEmail()">
|
||||
Send Authorization E-mail
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" ng-disabled="creating">Cancel</button>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
<!-- Normal button bar -->
|
||||
<div class="modal-footer" ng-if="status == '' || status == 'creating'">
|
||||
<button type="submit" class="btn btn-primary"
|
||||
ng-disabled="createForm.$invalid || !currentMethod.id || !currentEvent.id || creating">
|
||||
Create Notification
|
||||
|
|
|
@ -4758,12 +4758,13 @@ quayApp.directive('createExternalNotificationDialog', function () {
|
|||
'counter': '=counter',
|
||||
'notificationCreated': '¬ificationCreated'
|
||||
},
|
||||
controller: function($scope, $element, ExternalNotificationData, ApiService) {
|
||||
controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout) {
|
||||
$scope.currentEvent = null;
|
||||
$scope.currentMethod = null;
|
||||
$scope.creating = false;
|
||||
$scope.status = '';
|
||||
$scope.currentConfig = {};
|
||||
$scope.clearCounter = 0;
|
||||
$scope.unauthorizedEmail = false;
|
||||
|
||||
$scope.events = ExternalNotificationData.getSupportedEvents();
|
||||
$scope.methods = ExternalNotificationData.getSupportedMethods();
|
||||
|
@ -4775,10 +4776,35 @@ quayApp.directive('createExternalNotificationDialog', function () {
|
|||
$scope.setMethod = function(method) {
|
||||
$scope.currentConfig = {};
|
||||
$scope.currentMethod = method;
|
||||
$scope.unauthorizedEmail = false;
|
||||
};
|
||||
|
||||
$scope.createNotification = function() {
|
||||
$scope.creating = true;
|
||||
if (!$scope.currentConfig.email) {
|
||||
$scope.performCreateNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.status = 'checking-email';
|
||||
$scope.checkEmailAuthorization();
|
||||
};
|
||||
|
||||
$scope.checkEmailAuthorization = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'email': $scope.currentConfig.email
|
||||
};
|
||||
|
||||
ApiService.checkRepoEmailAuthorized(null, params).then(function(resp) {
|
||||
$scope.handleEmailCheck(resp.confirmed);
|
||||
}, function(resp) {
|
||||
$scope.handleEmailCheck(false);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.performCreateNotification = function() {
|
||||
$scope.status = 'creating';
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||
};
|
||||
|
@ -4790,21 +4816,58 @@ quayApp.directive('createExternalNotificationDialog', function () {
|
|||
};
|
||||
|
||||
ApiService.createRepoNotification(data, params).then(function(resp) {
|
||||
$scope.creating = false;
|
||||
$scope.status = '';
|
||||
$scope.notificationCreated({'notification': resp});
|
||||
$('#createNotificationModal').modal('hide');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.handleEmailCheck = function(isAuthorized) {
|
||||
if (isAuthorized) {
|
||||
$scope.performCreateNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.status == 'authorizing-email-sent') {
|
||||
$scope.watchEmail();
|
||||
} else {
|
||||
$scope.status = 'unauthorized-email';
|
||||
}
|
||||
|
||||
$scope.unauthorizedEmail = true;
|
||||
};
|
||||
|
||||
$scope.sendAuthEmail = function() {
|
||||
$scope.status = 'authorizing-email';
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'email': $scope.currentConfig.email
|
||||
};
|
||||
|
||||
ApiService.sendAuthorizeRepoEmail(null, params).then(function(resp) {
|
||||
$scope.status = 'authorizing-email-sent';
|
||||
$scope.watchEmail();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.watchEmail = function() {
|
||||
// TODO: change this to SSE?
|
||||
$timeout(function() {
|
||||
$scope.checkEmailAuthorization();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
$scope.$watch('counter', function(counter) {
|
||||
if (counter) {
|
||||
$scope.clearCounter++;
|
||||
$scope.creating = false;
|
||||
$scope.status = '';
|
||||
$scope.currentEvent = null;
|
||||
$scope.currentMethod = null;
|
||||
$scope.unauthorizedEmail = false;
|
||||
$('#createNotificationModal').modal({});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
|
14
templates/message.html
Normal file
14
templates/message.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<html>
|
||||
<title>Quay.io</title>
|
||||
<head>
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css">
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
||||
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" style="margin-top: 20px">
|
||||
<img src="/static/img/quay-logo.png">
|
||||
<h5>{{ message | safe }}</h5>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
|
@ -18,6 +18,7 @@ from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
|||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||
BuildTriggerList, BuildTriggerAnalyze)
|
||||
from endpoints.api.repoemail import RepositoryAuthorizedEmail
|
||||
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
||||
Signin, User, UserAuthorizationList, UserAuthorization)
|
||||
|
@ -2586,7 +2587,8 @@ class TestRepositoryNotificationListDevtableShared(ApiTestCase):
|
|||
self._run_test('POST', 403, 'reader', {})
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 201, 'devtable', {'event': 'repo_push', 'method': 'email', 'config': {}})
|
||||
self._run_test('POST', 400, 'devtable', {'event': 'repo_push', 'method': 'email',
|
||||
'config': {'email': 'a@b.com'}})
|
||||
|
||||
|
||||
class TestRepositoryNotificationListBuynlargeOrgrepo(ApiTestCase):
|
||||
|
@ -2616,7 +2618,102 @@ class TestRepositoryNotificationListBuynlargeOrgrepo(ApiTestCase):
|
|||
self._run_test('POST', 403, 'reader', {})
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 201, 'devtable', {'event': 'repo_push', 'method': 'email', 'config': {}})
|
||||
self._run_test('POST', 400, 'devtable', {'event': 'repo_push', 'method': 'email',
|
||||
'config': {'email': 'a@b.com'}})
|
||||
|
||||
|
||||
class TestRepositoryAuthorizedEmailPublicPublicrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(RepositoryAuthorizedEmail, repository="public/publicrepo",
|
||||
email="jschorr@devtable.com")
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 403, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 403, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 403, 'devtable', None)
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, {})
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', {})
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', {})
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 403, 'devtable', {})
|
||||
|
||||
|
||||
class TestRepositoryAuthorizedEmailDevtableSharedrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(RepositoryAuthorizedEmail, repository="devtable/shared",
|
||||
email="jschorr@devtable.com")
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 403, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 403, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 404, 'devtable', None)
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, {})
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', {})
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', {})
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 200, 'devtable', {})
|
||||
|
||||
|
||||
class TestRepositoryAuthorizedEmailBuynlargeOrgrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(RepositoryAuthorizedEmail, repository="buynlarge/orgrepo",
|
||||
email="jschorr@devtable.com")
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 403, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 403, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 404, 'devtable', None)
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, {})
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', {})
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', {})
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 200, 'devtable', {})
|
||||
|
||||
|
||||
|
||||
class TestRepositoryTokenListPublicPublicrepo(ApiTestCase):
|
||||
|
|
|
@ -20,6 +20,7 @@ from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
|||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||
BuildTriggerList, BuildTriggerAnalyze)
|
||||
from endpoints.api.repoemail import RepositoryAuthorizedEmail
|
||||
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
|
||||
UserAuthorizationList, UserAuthorization)
|
||||
|
@ -1082,6 +1083,50 @@ class TestRequestRepoBuild(ApiTestCase):
|
|||
expected_code=403)
|
||||
|
||||
|
||||
class TestRepositoryEmail(ApiTestCase):
|
||||
def test_emailnotauthorized(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
# Verify the e-mail address is not authorized.
|
||||
json = self.getResponse(RepositoryAuthorizedEmail,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', email='test@example.com'),
|
||||
expected_code=404)
|
||||
|
||||
def test_emailnotauthorized_butsent(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
# Verify the e-mail address is not authorized.
|
||||
json = self.getJsonResponse(RepositoryAuthorizedEmail,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', email='jschorr+other@devtable.com'))
|
||||
|
||||
self.assertEquals(False, json['confirmed'])
|
||||
self.assertEquals(ADMIN_ACCESS_USER, json['namespace'])
|
||||
self.assertEquals('simple', json['repository'])
|
||||
|
||||
|
||||
def test_emailauthorized(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
# Verify the e-mail address is authorized.
|
||||
json = self.getJsonResponse(RepositoryAuthorizedEmail,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', email='jschorr@devtable.com'))
|
||||
|
||||
self.assertEquals(True, json['confirmed'])
|
||||
self.assertEquals(ADMIN_ACCESS_USER, json['namespace'])
|
||||
self.assertEquals('simple', json['repository'])
|
||||
|
||||
|
||||
def test_send_email_authorization(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
# Send the email.
|
||||
json = self.postJsonResponse(RepositoryAuthorizedEmail,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', email='jschorr+foo@devtable.com'))
|
||||
|
||||
self.assertEquals(False, json['confirmed'])
|
||||
self.assertEquals(ADMIN_ACCESS_USER, json['namespace'])
|
||||
self.assertEquals('simple', json['repository'])
|
||||
|
||||
|
||||
class TestRepositoryNotifications(ApiTestCase):
|
||||
def test_webhooks(self):
|
||||
|
|
|
@ -57,6 +57,14 @@ Thanks and have a great day!<br>
|
|||
"""
|
||||
|
||||
|
||||
AUTH_FORREPO_MESSAGE = """
|
||||
A request has been made to send notifications to this email address for the <a href="https://quay.io">Quay.io</a> repository <a href="https://quay.io/repository/%s/%s">%s/%s</a>.
|
||||
<br>
|
||||
To confirm this email address, please click the following link:<br>
|
||||
<a href="https://quay.io/authrepoemail?code=%s">https://quay.io/authrepoemail?code=%s</a>
|
||||
"""
|
||||
|
||||
|
||||
SUBSCRIPTION_CHANGE_TITLE = 'Subscription Change - {0} {1}'
|
||||
|
||||
|
||||
|
@ -76,6 +84,14 @@ def send_confirmation_email(username, email, token):
|
|||
mail.send(msg)
|
||||
|
||||
|
||||
def send_repo_authorization_email(namespace, repository, email, token):
|
||||
msg = Message('Quay.io Notification: Please confirm your email.',
|
||||
sender='support@quay.io', # Why do I need this?
|
||||
recipients=[email])
|
||||
msg.html = AUTH_FORREPO_MESSAGE % (namespace, repository, namespace, repository, token, token)
|
||||
mail.send(msg)
|
||||
|
||||
|
||||
def send_recovery_email(email, token):
|
||||
msg = Message('Quay.io account recovery.',
|
||||
sender='support@quay.io', # Why do I need this?
|
||||
|
|
Reference in a new issue