Source code for stanford.mais.account.validate

# vim: ts=4 sw=4 et
# -*- coding: utf-8 -*-

# © 2021 The Board of Trustees of the Leland Stanford Junior University.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

from collections.abc import Collection
import dataclasses
import functools
import logging
import re
import stanford.mais.account

# Set up logging
logger = logging.getLogger(__name__)
debug = logger.debug
info = logger.info

__all__ = (
    'validate',
    'AccountValidationResults',
)

# In __all__, we list `validate` first.  But in the code, we define the results
# class first.  That is so that we can reference the class name directly in
# `validate`.

[docs] @dataclasses.dataclass(frozen=True, slots=True, weakref_slot=True) class AccountValidationResults: """Results of doing an account validation. This class contains the result of calling :func:`validate`. """ raw: str | None """ The raw input string provided for validation. This is only provided when a string was provided to :func:`validate`. """ raw_set: Collection[str] """ The raw input provided for validation. If a string was provided to :func:`validate`, then this is the raw input after being split. If the input to :func:`validate` was a set, then this is that set. Otherwise, this is the input to :func:`validate`, but as a set. The set union of ``full``, ``base``, ``inactive``, and ``unknown`` is equal to this. """ full: Collection[str] """ The set of active full (or full-sponsored) SUNetIDs found in ``raw_set``. """ base: Collection[str] """ The set of active base (or base-sponsored) SUNetIDs found in ``raw_set``. """ inactive: Collection[str] """ The set of inactive SUNetIDs found in ``raw_set``. """ unknown: Collection[str] """ The set of entries from ``raw_set`` that are not SUNetIDs. This includes uids that are functional accounts. """
# For validation, we will have three functions: # * Our first function handles strings, and splitting them up. # This is also the named function, so our documentation is here. # * The second function handles collections of strings. # * The third function does the actual validation work. # We have three functions because each function needs to process the validation # results a little bit, before returning them to the client.
[docs] @functools.singledispatch def validate( raw: str, client: stanford.mais.account.AccountClient, ) -> AccountValidationResults: """Given a list of SUNetIDs—as a :class:`str`, :class:`list`, :class:`tuple`, :class:`set`, or :class:`frozenset`—validate and check status. This takes a list of SUNetIDs, and returns a list of SUNetIDs which have been checked against the Accounts API for both activeness and service level. The returned result shows which SUNetIDs are active full (or full-sponsored), active base (or base-sponsored), or inactive. All other entries (including those representing functional accounts) are rejected as "unknown". If the input is a string, then the input string may be separated by commas, and/or whitespace, (where "whitespace" is a space, newline/linefeed, form feed, carriage return, tab/horizontal tab, or vertical tab character). If the input is a list, tuple, or set; the function assumes that all whitespace etc. have been removed. .. note:: "List, tuple, set, or frozenset" is used instead of the generic :class:`~collections.abc.Collection` because a :class:`str` is also a collection (of one-character :class:`str`). See `typing issue #256 <https://github.com/python/typing/issues/256>`_ for the discussion around this issue. This is designed to catch most exceptions. Exceptions related to validation (for example, attempting to validate an obviously-invalid SUNetID like ``ab$#``) will result in the 'SUNetID' being added to the `unknown` list, instead of throwing an exception. The only exceptions that should be expected from this function are ones related to API issues. :param raw: The list of SUNetIDs. If a :class:`str`, then the list must be either comma- and/or whitespace-separated. :param client: An :class:`~stanford.mais.account.AccountClient` to connect to the Account API. :return: The results of the validation. See :class:`AccountValidationResults` for details. :raises ChildProcessError: Something went wrong on the server side (a 400 or 500 error was returned). :raises PermissionError: You did not use a valid certificate, or do not have permissions to perform the operation. :raises requests.Timeout: The MaIS Workgroup API did not respond in time. """ debug('In validate with str') debug(f"Validation input: {raw}") # Start by spliting on whitespace and comma. # NOTE: Repeated instances of whitespace/separators will make empty entries. raw_list = re.split(r'\s|,', raw) # Filter out all empty entries, and remove duplicates by using the set. debug(f"Split list pre-filter has {len(raw_list)} items.") raw_list_filtered = set(filter( lambda item: len(item)>0, raw_list )) # Validate the list entries. debug(f"Post-filter list has {len(raw_list_filtered)} items.") result = _validate(raw_list_filtered, client) # Add in our raw string, and we're done! return AccountValidationResults( raw=raw, raw_set=result.raw_set, full=result.full, base=result.base, inactive=result.inactive, unknown=result.unknown, )
# At this time, MyPy has a problem with the type-checking single-dispatch # functions. See https://github.com/python/mypy/issues/13040 @validate.register(list) @validate.register(tuple) @validate.register(set) @validate.register(frozenset) def _( # type: ignore[misc] raw: list[str] | tuple[str] | set[str] | frozenset[str], client: stanford.mais.account.AccountClient, ) -> AccountValidationResults: """ (This is a single-dispatch function. See the documentation above! """ debug('In validate with list/tuple/set') debug('Input: ' + ','.join(raw)) debug(f"Input has {len(raw)} items.") # If we were not given a set, then convert it into a set. if not isinstance(raw, set): raw = set(raw) # Get the results result = _validate(raw, client) # Add in our original raw, and we're done! return AccountValidationResults( raw=None, raw_set=raw, full=result.full, base=result.base, inactive=result.inactive, unknown=result.unknown, ) # Do the actual validation here! def _validate( sunetids: set[str], client: stanford.mais.account.AccountClient, ) -> AccountValidationResults: # Components of the output full = set() base = set() inactive = set() unknown = set() # Limit our accounts to only people people = client.only_people() for sunetid in sunetids: # Catch unknown entries try: account = people.get(sunetid) debug(f"Account {sunetid} exists.") except (ValueError, IndexError, KeyError): debug(f"Account {sunetid} not found.") unknown.add(sunetid) continue # Overwrite the input SUNetID with the normalized one. sunetid = account.sunetid # Next, catch inactives if account.is_active is False: debug(f"Account {sunetid} NOT active.") inactive.add(sunetid) continue # Finally, sort into full or base if account.is_full: debug(f"Account {sunetid} is FULL") full.add(sunetid) else: debug(f"Account {sunetid} is base") base.add(sunetid) # Return the structure debug(f"Validation results: full={len(full)} base={len(base)} inactive={len(inactive)} unknown={len(unknown)}") return AccountValidationResults( raw=None, raw_set=sunetids, full=frozenset(full), base=frozenset(base), inactive=frozenset(inactive), unknown=frozenset(unknown), )