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
|
@ -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']
|
||||
|
|
Reference in a new issue