implement api for expiring members

This commit is contained in:
Rio Liu 2021-10-16 13:04:35 -04:00
parent f1c0ce3dd6
commit 67a7cc2d05
6 changed files with 70 additions and 3 deletions

View File

@ -87,3 +87,9 @@ class ILDAPService(Interface):
be returned along with their new programs, in the same format
described above.
"""
def get_expiring_users(self) -> List[IUser]:
"""
Retrieves members whose term or nonMemberTerm does not contain the
current or the last term.
"""

View File

@ -28,6 +28,9 @@ class Term:
s_term = c + str(dt.year)
return Term(s_term)
def start_month(self):
return self.seasons.index(self.s_term[0])
def __add__(self, other):
assert type(other) is int
c = self.s_term[0]
@ -40,6 +43,7 @@ class Term:
return Term(s_term)
def __sub__(self, other):
assert type(other) is int
return self.__add__(-other)
def __eq__(self, other):

View File

@ -1,9 +1,9 @@
from flask import Blueprint, request
from flask import Blueprint, request, json
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, development_only
create_streaming_response, development_only, is_truthy
from ceo_common.errors import BadRequest
from ceo_common.interfaces import ILDAPService
from ceod.transactions.members import (
@ -86,9 +86,11 @@ def renew_user(username: str):
user = ldap_srv.get_user(username)
if body.get('terms'):
user.add_terms(body['terms'])
user.set_expired(False)
return {'terms_added': body['terms']}
elif body.get('non_member_terms'):
user.add_non_member_terms(body['non_member_terms'])
user.set_expired(False)
return {'non_member_terms_added': body['non_member_terms']}
else:
raise BadRequest('Must specify either terms or non-member terms')
@ -110,3 +112,20 @@ def reset_user_password(username: str):
def delete_user(username: str):
txn = DeleteMemberTransaction(username)
return create_streaming_response(txn)
@bp.route('/expire', methods=['POST'])
@authz_restrict_to_syscom
def expire_users():
dry_run = False
if is_truthy(request.args.get('dry_run', 'false')):
dry_run = True
ldap_srv = component.getUtility(ILDAPService)
members = ldap_srv.get_expiring_users()
if not dry_run:
for member in members:
member.set_expired(True)
return json.jsonify([member.uid for member in members])

View File

@ -140,4 +140,4 @@ def development_only(f: Callable) -> Callable:
def is_truthy(s: str) -> bool:
return s.lower() in ['yes', 'true']
return s.lower() in ['yes', 'true', '1']

View File

@ -1,6 +1,7 @@
import contextlib
import grp
import pwd
import datetime
from typing import Union, Dict, List
from flask import g
@ -13,6 +14,7 @@ from ceo_common.errors import UserNotFoundError, GroupNotFoundError, \
UserAlreadyExistsError, GroupAlreadyExistsError
from ceo_common.interfaces import ILDAPService, IConfig, \
IUser, IGroup, IUWLDAPService
from ceo_common.model import Term
from .User import User
from .Group import Group
@ -243,6 +245,30 @@ class LDAPService:
except ldap3.core.exceptions.LDAPEntryAlreadyExistsResult:
raise GroupAlreadyExistsError()
def get_expiring_users(self) -> List[IUser]:
query = []
term = Term.current()
query.append(f'term={term}')
query.append(f'nonMemberTerm={term}')
# Include last term too if the new term just started
dt = datetime.datetime.now()
if dt.month == term.start_month():
last_term = term - 1
query.append(f'term={last_term}')
query.append(f'nonMemberTerm={last_term}')
query = '(!(|(' + ')('.join(query) + ')))'
conn = self._get_ldap_conn()
conn.search(
self.ldap_users_base,
query,
attributes=ldap3.ALL_ATTRIBUTES,
search_scope=ldap3.LEVEL)
return [User.deserialize_from_ldap(entry) for entry in conn.entries]
@contextlib.contextmanager
def entry_ctx_for_group(self, group: IGroup):
entry = self._get_writable_entry_for_group(group)

View File

@ -33,6 +33,7 @@ class User:
is_club_rep: Union[bool, None] = None,
is_club: bool = False,
ldap3_entry: Union[ldap3.Entry, None] = None,
expired: bool = False,
):
cfg = component.getUtility(IConfig)
@ -66,6 +67,7 @@ class User:
else:
self.is_club_rep = is_club_rep
self.ldap3_entry = ldap3_entry
self.expired = expired
self.ldap_srv = component.getUtility(ILDAPService)
self.krb_srv = component.getUtility(IKerberosService)
@ -82,6 +84,7 @@ class User:
'is_club': self.is_club(),
'is_club_rep': self.is_club_rep,
'program': self.program or 'Unknown',
'expired': self.expired,
}
if self.sn and self.given_name:
data['sn'] = self.sn
@ -155,6 +158,7 @@ class User:
mail_local_addresses=attrs.get('mailLocalAddress'),
is_club_rep=attrs.get('isClubRep', [False])[0],
is_club=('club' in attrs['objectClass']),
expired=attrs.get('shadowExpire', 0) != 0,
ldap3_entry=entry,
)
@ -205,3 +209,11 @@ class User:
current_term = Term.current()
most_recent_term = max(map(Term, self.terms))
return most_recent_term >= current_term
def set_expired(self, expired: bool):
with self.ldap_srv.entry_ctx_for_user(self) as entry:
if expired:
entry.shadowExpire = 1
else:
entry.shadowExpire.remove()
self.expired = expired