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()
component.provideUtility(uwldap_srv, IUWLDAPService)
from ceod.api import uwldap
app.register_blueprint(uwldap.bp, url_prefix='/api/uwldap')
@app.route('/ping')
def ping():
"""Health check"""

View File

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

View File

@ -3,7 +3,7 @@ from zope import component
from .utils import authz_restrict_to_staff, authz_restrict_to_syscom, \
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.interfaces import ILDAPService
from ceod.transactions.members import (
@ -71,11 +71,11 @@ def renew_user(username: str):
terms=body.get('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'])
@authz_restrict_to_syscom
def reset_user_password(username: str):
txn = ResetPasswordTransaction(username)
return create_streaming_response(txn)
return create_sync_response(txn)

View File

@ -3,55 +3,18 @@ import grp
import json
import os
import pwd
import socket
import traceback
from typing import Callable, List
from flask import current_app
from flask_kerberos import requires_authentication
from zope import component
from ceo_common.logger_factory import logger_factory
from ceo_common.interfaces import IConfig
from ceod.transactions import AbstractTransaction
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:
"""
Like requires_authentication, but strips the realm out of the principal string.
@ -140,3 +103,18 @@ def create_streaming_response(txn: AbstractTransaction):
}) + '\n'
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
@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
UWLDAPRecord.
@ -28,3 +28,10 @@ class UWLDAPRecord:
program=data.get('ou', [None])[0],
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}')
if not results:
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)
yield 'subscribe_to_mailing_list'
self.finish('success')
def rollback(self):
# nothing to do, since there was only one operation
pass
self.finish('OK')

View File

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