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()
|
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,
|
all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility,
|
||||||
RepositoryTag, EmailConfirmation, FederatedLogin, LoginService, QueueItem,
|
RepositoryTag, EmailConfirmation, FederatedLogin, LoginService, QueueItem,
|
||||||
RepositoryBuild, Team, TeamMember, TeamRole, LogEntryKind, LogEntry,
|
RepositoryBuild, Team, TeamMember, TeamRole, LogEntryKind, LogEntry,
|
||||||
PermissionPrototype, ImageStorage, BuildTriggerService, RepositoryBuildTrigger,
|
PermissionPrototype, ImageStorage, BuildTriggerService, RepositoryBuildTrigger,
|
||||||
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind,
|
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind,
|
||||||
Notification, ImageStorageLocation, ImageStoragePlacement,
|
Notification, ImageStorageLocation, ImageStoragePlacement,
|
||||||
ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification]
|
ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification,
|
||||||
|
RepositoryAuthorizedEmail]
|
||||||
|
|
|
@ -1763,3 +1763,39 @@ def check_health():
|
||||||
return found_count > 0
|
return found_count > 0
|
||||||
except:
|
except:
|
||||||
return False
|
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.prototype
|
||||||
import endpoints.api.repository
|
import endpoints.api.repository
|
||||||
import endpoints.api.repositorynotification
|
import endpoints.api.repositorynotification
|
||||||
|
import endpoints.api.repoemail
|
||||||
import endpoints.api.repotoken
|
import endpoints.api.repotoken
|
||||||
import endpoints.api.robot
|
import endpoints.api.robot
|
||||||
import endpoints.api.search
|
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
|
import json
|
||||||
|
|
||||||
from flask import request
|
from flask import request, abort
|
||||||
|
|
||||||
from app import notification_queue
|
from app import notification_queue
|
||||||
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
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.notificationevent import NotificationEvent
|
||||||
|
from endpoints.notificationmethod import (NotificationMethod,
|
||||||
|
CannotValidateNotificationMethodException)
|
||||||
from data import model
|
from data import model
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,6 +64,15 @@ class RepositoryNotificationList(RepositoryParamResource):
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
json = request.get_json()
|
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'],
|
notification = model.create_repo_notification(repo, json['event'], json['method'],
|
||||||
json['config'])
|
json['config'])
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,10 @@ logger = logging.getLogger(__name__)
|
||||||
class InvalidNotificationMethodException(Exception):
|
class InvalidNotificationMethodException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class CannotValidateNotificationMethodException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NotificationMethod(object):
|
class NotificationMethod(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
@ -25,6 +29,13 @@ class NotificationMethod(object):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
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):
|
def perform(self, notification, event_handler, notification_data):
|
||||||
"""
|
"""
|
||||||
Performs the notification method.
|
Performs the notification method.
|
||||||
|
@ -49,36 +60,32 @@ class QuayNotificationMethod(NotificationMethod):
|
||||||
def method_name(cls):
|
def method_name(cls):
|
||||||
return 'quay_notification'
|
return 'quay_notification'
|
||||||
|
|
||||||
def perform(self, notification, event_handler, notification_data):
|
def validate(self, repository, config_data):
|
||||||
config_data = json.loads(notification.config_json)
|
status, err_message, target_users = self.find_targets(repository, config_data)
|
||||||
repository_id = notification_data['repository_id']
|
if err_message:
|
||||||
repository = model.lookup_repository(repository_id)
|
raise CannotValidateNotificationMethodException(err_message)
|
||||||
if not repository:
|
|
||||||
# Probably deleted.
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Lookup the target user or team to which we'll send the notification.
|
def find_targets(self, repository, config_data):
|
||||||
target_info = config_data['target']
|
target_info = config_data['target']
|
||||||
target_users = []
|
|
||||||
|
|
||||||
if target_info['kind'] == 'user':
|
if target_info['kind'] == 'user':
|
||||||
target = model.get_user(target_info['name'])
|
target = model.get_user(target_info['name'])
|
||||||
if not target:
|
if not target:
|
||||||
# Just to be safe.
|
# 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':
|
elif target_info['kind'] == 'org':
|
||||||
target = model.get_organization(target_info['name'])
|
target = model.get_organization(target_info['name'])
|
||||||
if not target:
|
if not target:
|
||||||
# Just to be safe.
|
# 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.
|
# Only repositories under the organization can cause notifications to that org.
|
||||||
if target_info['name'] != repository.namespace:
|
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':
|
elif target_info['kind'] == 'team':
|
||||||
# Lookup the team.
|
# Lookup the team.
|
||||||
team = None
|
team = None
|
||||||
|
@ -86,13 +93,27 @@ class QuayNotificationMethod(NotificationMethod):
|
||||||
team = model.get_organization_team(repository.namespace, target_info['name'])
|
team = model.get_organization_team(repository.namespace, target_info['name'])
|
||||||
except model.InvalidTeamException:
|
except model.InvalidTeamException:
|
||||||
# Probably deleted.
|
# Probably deleted.
|
||||||
return True
|
return (True, 'Unknown team %s' % target_info['name'], None)
|
||||||
|
|
||||||
# Lookup the team's members
|
# 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 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,
|
model.create_notification(event_handler.event_name(), target_user,
|
||||||
metadata=notification_data['event_data'])
|
metadata=notification_data['event_data'])
|
||||||
return True
|
return True
|
||||||
|
@ -103,6 +124,18 @@ class EmailMethod(NotificationMethod):
|
||||||
def method_name(cls):
|
def method_name(cls):
|
||||||
return 'email'
|
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):
|
def perform(self, notification, event_handler, notification_data):
|
||||||
config_data = json.loads(notification.config_json)
|
config_data = json.loads(notification.config_json)
|
||||||
email = config_data.get('email', '')
|
email = config_data.get('email', '')
|
||||||
|
@ -129,6 +162,11 @@ class WebhookMethod(NotificationMethod):
|
||||||
def method_name(cls):
|
def method_name(cls):
|
||||||
return 'webhook'
|
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):
|
def perform(self, notification, event_handler, notification_data):
|
||||||
config_data = json.loads(notification.config_json)
|
config_data = json.loads(notification.config_json)
|
||||||
url = config_data.get('url', '')
|
url = config_data.get('url', '')
|
||||||
|
|
|
@ -214,6 +214,26 @@ def receipt():
|
||||||
abort(404)
|
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'])
|
@web.route('/confirm', methods=['GET'])
|
||||||
def confirm_email():
|
def confirm_email():
|
||||||
code = request.values['code']
|
code = request.values['code']
|
||||||
|
|
|
@ -424,6 +424,12 @@ def populate_database():
|
||||||
'build_subdir': '',
|
'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,
|
build2 = model.create_repository_build(building, token, job_config,
|
||||||
'68daeebd-a5b9-457f-80a0-4363b882f8ea',
|
'68daeebd-a5b9-457f-80a0-4363b882f8ea',
|
||||||
'build-name', trigger)
|
'build-name', trigger)
|
||||||
|
|
|
@ -10,9 +10,26 @@
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<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>
|
<tr>
|
||||||
<td style="width: 120px">When this occurs:</td>
|
<td style="width: 120px">When this occurs:</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -59,7 +76,9 @@
|
||||||
<td>{{ field.title }}:</td>
|
<td>{{ field.title }}:</td>
|
||||||
<td>
|
<td>
|
||||||
<div ng-switch on="field.type">
|
<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="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>
|
<input type="text" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="string" required>
|
||||||
<div class="entity-search" namespace="repository.namespace"
|
<div class="entity-search" namespace="repository.namespace"
|
||||||
|
@ -87,7 +106,17 @@
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- Normal button bar -->
|
||||||
|
<div class="modal-footer" ng-if="status == '' || status == 'creating'">
|
||||||
<button type="submit" class="btn btn-primary"
|
<button type="submit" class="btn btn-primary"
|
||||||
ng-disabled="createForm.$invalid || !currentMethod.id || !currentEvent.id || creating">
|
ng-disabled="createForm.$invalid || !currentMethod.id || !currentEvent.id || creating">
|
||||||
Create Notification
|
Create Notification
|
||||||
|
|
|
@ -4758,12 +4758,13 @@ quayApp.directive('createExternalNotificationDialog', function () {
|
||||||
'counter': '=counter',
|
'counter': '=counter',
|
||||||
'notificationCreated': '¬ificationCreated'
|
'notificationCreated': '¬ificationCreated'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, ExternalNotificationData, ApiService) {
|
controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout) {
|
||||||
$scope.currentEvent = null;
|
$scope.currentEvent = null;
|
||||||
$scope.currentMethod = null;
|
$scope.currentMethod = null;
|
||||||
$scope.creating = false;
|
$scope.status = '';
|
||||||
$scope.currentConfig = {};
|
$scope.currentConfig = {};
|
||||||
$scope.clearCounter = 0;
|
$scope.clearCounter = 0;
|
||||||
|
$scope.unauthorizedEmail = false;
|
||||||
|
|
||||||
$scope.events = ExternalNotificationData.getSupportedEvents();
|
$scope.events = ExternalNotificationData.getSupportedEvents();
|
||||||
$scope.methods = ExternalNotificationData.getSupportedMethods();
|
$scope.methods = ExternalNotificationData.getSupportedMethods();
|
||||||
|
@ -4775,10 +4776,35 @@ quayApp.directive('createExternalNotificationDialog', function () {
|
||||||
$scope.setMethod = function(method) {
|
$scope.setMethod = function(method) {
|
||||||
$scope.currentConfig = {};
|
$scope.currentConfig = {};
|
||||||
$scope.currentMethod = method;
|
$scope.currentMethod = method;
|
||||||
|
$scope.unauthorizedEmail = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.createNotification = function() {
|
$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 = {
|
var params = {
|
||||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||||
};
|
};
|
||||||
|
@ -4790,18 +4816,55 @@ quayApp.directive('createExternalNotificationDialog', function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.createRepoNotification(data, params).then(function(resp) {
|
ApiService.createRepoNotification(data, params).then(function(resp) {
|
||||||
$scope.creating = false;
|
$scope.status = '';
|
||||||
$scope.notificationCreated({'notification': resp});
|
$scope.notificationCreated({'notification': resp});
|
||||||
$('#createNotificationModal').modal('hide');
|
$('#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) {
|
$scope.$watch('counter', function(counter) {
|
||||||
if (counter) {
|
if (counter) {
|
||||||
$scope.clearCounter++;
|
$scope.clearCounter++;
|
||||||
$scope.creating = false;
|
$scope.status = '';
|
||||||
$scope.currentEvent = null;
|
$scope.currentEvent = null;
|
||||||
$scope.currentMethod = null;
|
$scope.currentMethod = null;
|
||||||
|
$scope.unauthorizedEmail = false;
|
||||||
$('#createNotificationModal').modal({});
|
$('#createNotificationModal').modal({});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
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,
|
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||||
BuildTriggerList, BuildTriggerAnalyze)
|
BuildTriggerList, BuildTriggerAnalyze)
|
||||||
|
from endpoints.api.repoemail import RepositoryAuthorizedEmail
|
||||||
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
||||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
||||||
Signin, User, UserAuthorizationList, UserAuthorization)
|
Signin, User, UserAuthorizationList, UserAuthorization)
|
||||||
|
@ -2586,7 +2587,8 @@ class TestRepositoryNotificationListDevtableShared(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'reader', {})
|
self._run_test('POST', 403, 'reader', {})
|
||||||
|
|
||||||
def test_post_devtable(self):
|
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):
|
class TestRepositoryNotificationListBuynlargeOrgrepo(ApiTestCase):
|
||||||
|
@ -2616,7 +2618,102 @@ class TestRepositoryNotificationListBuynlargeOrgrepo(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'reader', {})
|
self._run_test('POST', 403, 'reader', {})
|
||||||
|
|
||||||
def test_post_devtable(self):
|
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):
|
class TestRepositoryTokenListPublicPublicrepo(ApiTestCase):
|
||||||
|
|
|
@ -20,6 +20,7 @@ from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
||||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||||
BuildTriggerList, BuildTriggerAnalyze)
|
BuildTriggerList, BuildTriggerAnalyze)
|
||||||
|
from endpoints.api.repoemail import RepositoryAuthorizedEmail
|
||||||
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
||||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
|
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
|
||||||
UserAuthorizationList, UserAuthorization)
|
UserAuthorizationList, UserAuthorization)
|
||||||
|
@ -1082,6 +1083,50 @@ class TestRequestRepoBuild(ApiTestCase):
|
||||||
expected_code=403)
|
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):
|
class TestRepositoryNotifications(ApiTestCase):
|
||||||
def test_webhooks(self):
|
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}'
|
SUBSCRIPTION_CHANGE_TITLE = 'Subscription Change - {0} {1}'
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,6 +84,14 @@ def send_confirmation_email(username, email, token):
|
||||||
mail.send(msg)
|
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):
|
def send_recovery_email(email, token):
|
||||||
msg = Message('Quay.io account recovery.',
|
msg = Message('Quay.io account recovery.',
|
||||||
sender='support@quay.io', # Why do I need this?
|
sender='support@quay.io', # Why do I need this?
|
||||||
|
|
Reference in a new issue