use create_sync_response

This commit is contained in:
Max Erenberg 2021-08-03 03:20:11 +00:00
parent c32e565f68
commit 7c67a07200
9 changed files with 53 additions and 57 deletions

View File

@ -65,6 +65,9 @@ def create_app(flask_config={}):
uwldap_srv = UWLDAPService() uwldap_srv = UWLDAPService()
component.provideUtility(uwldap_srv, IUWLDAPService) component.provideUtility(uwldap_srv, IUWLDAPService)
from ceod.api import uwldap
app.register_blueprint(uwldap.bp, url_prefix='/api/uwldap')
@app.route('/ping') @app.route('/ping')
def ping(): def ping():
"""Health check""" """Health check"""

View File

@ -1,6 +1,6 @@
from flask import Blueprint from flask import Blueprint
from .utils import authz_restrict_to_staff
from .utils import authz_restrict_to_staff, create_sync_response
from ceod.transactions.mailman import SubscribeMemberTransaction, UnsubscribeMemberTransaction from ceod.transactions.mailman import SubscribeMemberTransaction, UnsubscribeMemberTransaction
bp = Blueprint('mailman', __name__) bp = Blueprint('mailman', __name__)
@ -10,13 +10,11 @@ bp = Blueprint('mailman', __name__)
@authz_restrict_to_staff @authz_restrict_to_staff
def subscribe(mailing_list, username): def subscribe(mailing_list, username):
txn = SubscribeMemberTransaction(username, mailing_list) txn = SubscribeMemberTransaction(username, mailing_list)
txn.execute() return create_sync_response(txn)
return {'message': f"{username} successfully subscribed to {mailing_list}"}
@bp.route('/<mailing_list>/<username>', methods=['DELETE']) @bp.route('/<mailing_list>/<username>', methods=['DELETE'])
@authz_restrict_to_staff @authz_restrict_to_staff
def unsubscribe(mailing_list, username): def unsubscribe(mailing_list, username):
txn = UnsubscribeMemberTransaction(username, mailing_list) txn = UnsubscribeMemberTransaction(username, mailing_list)
txn.execute() return create_sync_response(txn)
return {'message': f"{username} successfully unsubscribed from {mailing_list}"}

View File

@ -3,7 +3,7 @@ from zope import component
from .utils import authz_restrict_to_staff, authz_restrict_to_syscom, \ from .utils import authz_restrict_to_staff, authz_restrict_to_syscom, \
user_is_in_group, requires_authentication_no_realm, \ user_is_in_group, requires_authentication_no_realm, \
create_streaming_response create_streaming_response, create_sync_response
from ceo_common.errors import UserNotFoundError from ceo_common.errors import UserNotFoundError
from ceo_common.interfaces import ILDAPService from ceo_common.interfaces import ILDAPService
from ceod.transactions.members import ( from ceod.transactions.members import (
@ -71,11 +71,11 @@ def renew_user(username: str):
terms=body.get('terms'), terms=body.get('terms'),
non_member_terms=body.get('non_member_terms'), non_member_terms=body.get('non_member_terms'),
) )
return create_streaming_response(txn) return create_sync_response(txn)
@bp.route('/<username>/pwreset', methods=['POST']) @bp.route('/<username>/pwreset', methods=['POST'])
@authz_restrict_to_syscom @authz_restrict_to_syscom
def reset_user_password(username: str): def reset_user_password(username: str):
txn = ResetPasswordTransaction(username) txn = ResetPasswordTransaction(username)
return create_streaming_response(txn) return create_sync_response(txn)

View File

@ -3,55 +3,18 @@ import grp
import json import json
import os import os
import pwd import pwd
import socket
import traceback import traceback
from typing import Callable, List from typing import Callable, List
from flask import current_app from flask import current_app
from flask_kerberos import requires_authentication from flask_kerberos import requires_authentication
from zope import component
from ceo_common.logger_factory import logger_factory from ceo_common.logger_factory import logger_factory
from ceo_common.interfaces import IConfig
from ceod.transactions import AbstractTransaction from ceod.transactions import AbstractTransaction
logger = logger_factory(__name__) logger = logger_factory(__name__)
def restrict_host(role: str) -> Callable[[Callable], Callable]:
"""
This is a function which returns a decorator.
It returns a 400 if the client makes a request to an endpoint
which is restricted to a different host.
:param role: a key in the app's config (e.g. 'ceod_admin_host')
which maps to a specific hostname
Example:
@app.route('/<mailing_list>/<username>', methods=['POST'])
@restrict_host('mailman_host')
def subscribe(mailing_list, username):
....
"""
hostname = socket.gethostname()
cfg = component.getUtility(IConfig)
desired_hostname = cfg.get(role)
def identity(f: Callable):
return f
def error_decorator(f: Callable):
@functools.wraps(f)
def wrapper(*args, **kwargs):
return {'error': f'Wrong host! Use {desired_hostname} instead'}, 400
return wrapper
if hostname == desired_hostname:
return identity
return error_decorator
def requires_authentication_no_realm(f: Callable) -> Callable: def requires_authentication_no_realm(f: Callable) -> Callable:
""" """
Like requires_authentication, but strips the realm out of the principal string. Like requires_authentication, but strips the realm out of the principal string.
@ -140,3 +103,18 @@ def create_streaming_response(txn: AbstractTransaction):
}) + '\n' }) + '\n'
return current_app.response_class(generate(), mimetype='text/plain') return current_app.response_class(generate(), mimetype='text/plain')
def create_sync_response(txn: AbstractTransaction):
"""
Runs the transaction synchronously and returns a JSON response.
"""
try:
txn.execute()
return {'result': txn.result}
except Exception as err:
logger.warning('Transaction failed:\n' + traceback.format_exc())
txn.rollback()
return {
'error': str(err),
}, 500

17
ceod/api/uwldap.py Normal file
View File

@ -0,0 +1,17 @@
from flask import Blueprint
from zope import component
from ceo_common.interfaces import IUWLDAPService
bp = Blueprint('uwldap', __name__)
@bp.route('/<username>')
def get_user(username: str):
uwldap_srv = component.getUtility(IUWLDAPService)
record = uwldap_srv.get(username)
if record is None:
return {
'error': 'user not found',
}, 404
return record.to_dict()

View File

@ -17,7 +17,7 @@ class UWLDAPRecord:
self.mail_local_addresses = mail_local_addresses self.mail_local_addresses = mail_local_addresses
@staticmethod @staticmethod
def deserialize_from_ldap(self, data: Dict[str, List[bytes]]): def deserialize_from_ldap(data: Dict[str, List[bytes]]):
""" """
Deserializes a dict returned from ldap.search_s() into a Deserializes a dict returned from ldap.search_s() into a
UWLDAPRecord. UWLDAPRecord.
@ -28,3 +28,10 @@ class UWLDAPRecord:
program=data.get('ou', [None])[0], program=data.get('ou', [None])[0],
mail_local_addresses=data['mailLocalAddress'], mail_local_addresses=data['mailLocalAddress'],
) )
def to_dict(self):
return {
'uid': self.uid,
'program': self.program,
'mail_local_addresses': self.mail_local_addresses,
}

View File

@ -20,4 +20,5 @@ class UWLDAPService:
results = conn.search_s(self.uwldap_base, ldap.SCOPE_SUBTREE, f'uid={username}') results = conn.search_s(self.uwldap_base, ldap.SCOPE_SUBTREE, f'uid={username}')
if not results: if not results:
return None return None
return UWLDAPRecord.deserialize_from_ldap(results[0]) _, data = results[0] # discard the dn
return UWLDAPRecord.deserialize_from_ldap(data)

View File

@ -26,8 +26,4 @@ class SubscribeMemberTransaction(AbstractTransaction):
self.mailman_srv.subscribe(self.address, self.mailing_list) self.mailman_srv.subscribe(self.address, self.mailing_list)
yield 'subscribe_to_mailing_list' yield 'subscribe_to_mailing_list'
self.finish('success') self.finish('OK')
def rollback(self):
# nothing to do, since there was only one operation
pass

View File

@ -26,8 +26,4 @@ class UnsubscribeMemberTransaction(AbstractTransaction):
self.mailman_srv.unsubscribe(self.address, self.mailing_list) self.mailman_srv.unsubscribe(self.address, self.mailing_list)
yield 'unsubscribe_to_mailing_list' yield 'unsubscribe_to_mailing_list'
self.finish('success') self.finish('OK')
def rollback(self):
# nothing to do, since there was only one operation
pass