implement renewals and password resets

This commit is contained in:
Max Erenberg 2021-08-02 08:01:13 +00:00
parent da14764687
commit c32e565f68
10 changed files with 126 additions and 13 deletions

View File

@ -4,3 +4,7 @@ class UserNotFoundError(Exception):
class GroupNotFoundError(Exception):
pass
class BadRequest(Exception):
pass

View File

@ -1,13 +1,16 @@
from flask import Blueprint, request
from zope import component
from .utils import authz_restrict_to_staff, user_is_in_group, \
requires_authentication_no_realm, create_streaming_response
from .utils import authz_restrict_to_staff, authz_restrict_to_syscom, \
user_is_in_group, requires_authentication_no_realm, \
create_streaming_response
from ceo_common.errors import UserNotFoundError
from ceo_common.interfaces import ILDAPService
from ceod.transactions.members import (
AddMemberTransaction,
ModifyMemberTransaction,
RenewMemberTransaction,
ResetPasswordTransaction,
)
bp = Blueprint('members', __name__)
@ -57,3 +60,22 @@ def patch_user(auth_user: str, username: str):
forwarding_addresses=body.get('forwarding_addresses'),
)
return create_streaming_response(txn)
@bp.route('/<username>/renew', methods=['POST'])
@authz_restrict_to_staff
def renew_user(username: str):
body = request.get_json(force=True)
txn = RenewMemberTransaction(
username,
terms=body.get('terms'),
non_member_terms=body.get('non_member_terms'),
)
return create_streaming_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)

View File

@ -7,7 +7,7 @@ from zope import component
from zope.interface import implementer
from .utils import strings_to_bytes, bytes_to_strings
from .validators import is_valid_shell
from .validators import is_valid_shell, is_valid_term
from ceo_common.interfaces import ILDAPService, IKerberosService, IFileService, \
IUser, IConfig, IMailmanService
@ -178,6 +178,9 @@ class User:
self.ldap_srv.modify_user(self, new_user)
def add_terms(self, terms: List[str]):
for term in terms:
if not is_valid_term(term):
raise Exception('%s is not a valid term' % term)
new_user = copy.copy(self)
new_user.terms = self.terms.copy()
new_user.terms.extend(terms)
@ -185,6 +188,9 @@ class User:
self.terms = new_user.terms
def add_non_member_terms(self, terms: List[str]):
for term in terms:
if not is_valid_term(term):
raise Exception('%s is not a valid term' % term)
new_user = copy.copy(self)
new_user.non_member_terms = self.non_member_terms.copy()
new_user.non_member_terms.extend(terms)

View File

@ -58,3 +58,9 @@ def is_valid_shell(shell: str) -> bool:
line.strip() for line in open('/etc/shells')
if line != '' and not line.isspace()
]
def is_valid_term(term: str) -> bool:
return len(term) == 5 and \
term[0] in ['s', 'f', 'w'] and \
term[1:5].isdigit()

View File

@ -41,7 +41,10 @@ class AbstractTransaction(ABC):
for _ in self.execute_iter():
pass
@abstractmethod
def rollback(self):
"""Roll back the transaction, when it fails."""
raise NotImplementedError()
"""
Roll back the transaction, when it fails.
If the transaction only has one operation, then there is no need
to implement this.
"""
pass

View File

@ -1,11 +1,10 @@
import base64
import os
import traceback
from typing import Union, List
from zope import component
from ..AbstractTransaction import AbstractTransaction
from .utils import gen_password
from ceo_common.interfaces import IConfig, IMailService
from ceo_common.logger_factory import logger_factory
from ceod.model import User, Group
@ -13,11 +12,6 @@ from ceod.model import User, Group
logger = logger_factory(__name__)
def gen_password() -> str:
"""Generate a temporary password."""
return base64.b64encode(os.urandom(18)).decode()
class AddMemberTransaction(AbstractTransaction):
"""Transaction to add a new club member."""

View File

@ -0,0 +1,42 @@
from typing import Union, List
from zope import component
from ..AbstractTransaction import AbstractTransaction
from ceo_common.errors import BadRequest
from ceo_common.interfaces import ILDAPService
class RenewMemberTransaction(AbstractTransaction):
"""Transaction to renew a user's terms or non-member terms."""
operations = [
'add_terms',
'add_non_member_terms',
]
def __init__(
self,
username: str,
terms: Union[List[str], None],
non_member_terms: Union[List[str], None],
):
super().__init__()
self.username = username
if (terms and non_member_terms) or not (terms or non_member_terms):
raise BadRequest('Must specify either terms or non-member terms')
self.terms = terms
self.non_member_terms = non_member_terms
self.ldap_srv = component.getUtility(ILDAPService)
def child_execute_iter(self):
user = self.ldap_srv.get_user(self.username)
if self.terms:
user.add_terms(self.terms)
yield 'add_terms'
elif self.non_member_terms:
user.add_non_member_terms(self.non_member_terms)
yield 'add_non_member_terms'
self.finish('OK')

View File

@ -0,0 +1,27 @@
from zope import component
from ..AbstractTransaction import AbstractTransaction
from .utils import gen_password
from ceo_common.interfaces import ILDAPService
class ResetPasswordTransaction(AbstractTransaction):
"""Transaction to reset a user's password."""
operations = [
'change_password',
]
def __init__(self, username: str):
super().__init__()
self.username = username
self.ldap_srv = component.getUtility(ILDAPService)
def child_execute_iter(self):
user = self.ldap_srv.get_user(self.username)
password = gen_password()
user.change_password(password)
yield 'change_password'
self.finish({'password': password})

View File

@ -1,2 +1,4 @@
from .AddMemberTransaction import AddMemberTransaction
from .ModifyMemberTransaction import ModifyMemberTransaction
from .RenewMemberTransaction import RenewMemberTransaction
from .ResetPasswordTransaction import ResetPasswordTransaction

View File

@ -0,0 +1,7 @@
import base64
import os
def gen_password() -> str:
"""Generate a temporary password."""
return base64.b64encode(os.urandom(18)).decode()