diff --git a/src/pas/plugins/authomatic/plugin.py b/src/pas/plugins/authomatic/plugin.py index 5ea3a35..10e64c1 100644 --- a/src/pas/plugins/authomatic/plugin.py +++ b/src/pas/plugins/authomatic/plugin.py @@ -1,3 +1,5 @@ +from time import time +import authomatic.core import requests from AccessControl import ClassSecurityInfo from AccessControl.class_init import InitializeClass @@ -18,11 +20,20 @@ from Products.PluggableAuthService.utils import createViewName from zope.event import notify from zope.interface import implementer +from plone.memoize import ram import logging from pas.plugins.authomatic.utils import authomatic_cfg +logging.basicConfig(level=logging.DEBUG) +reqlogger = logging.getLogger('urllib3') +reqlogger.setLevel(logging.DEBUG) + + +# calculate fibbonacci + + logger = logging.getLogger(__name__) tpl_dir = Path(__file__).parent.resolve() / "browser" @@ -43,6 +54,11 @@ def manage_addAuthomaticPlugin(context, id, title="", RESPONSE=None, **kw): __name__="addAuthomaticPlugin", ) +def _cachekey_ms_users(method, self, login): + return time() // (60 * 60), login + +def _cachekey_ms_users_inconsistent(method, self, query, properties): + return time() // (60 * 60), query, properties.items() if properties else None @implementer( IAuthomaticPlugin, @@ -59,6 +75,8 @@ class AuthomaticPlugin(BasePlugin): meta_type = "Authomatic Plugin" BasePlugin.manage_options + _ms_token = None + # Tell PAS not to swallow our exceptions _dont_swallow_my_exceptions = True @@ -84,7 +102,7 @@ def _provider_id(self, result): return (result.provider.name, result.user.id) @security.private - def lookup_identities(self, result): + def lookup_identities(self, result: authomatic.core.LoginResult): """looks up the UserIdentities by using the provider name and the userid at this provider """ @@ -188,6 +206,9 @@ def getPropertiesForUser(self, user, request=None): @security.private def _getMSAccessToken(self): + if self._ms_token and self._ms_token["expires"] > time(): + return self._ms_token["access_token"] + settings = authomatic_cfg() cfg = settings.get("microsoft") @@ -207,25 +228,70 @@ def _getMSAccessToken(self): token_data = response.json() #TODO: cache this and refresh when necessary - return token_data["access_token"] + self._ms_token = { "expires": time() + token_data["expires_in"] - 60 } + self._ms_token.update(token_data) + return self._ms_token["access_token"] @security.private - def queryMSApiUsers(self, _login=""): + @ram.cache(_cachekey_ms_users) + def queryMSApiUsers(self, login=""): pluginid = self.getId() token = self._getMSAccessToken() - url = "https://graph.microsoft.com/v1.0/users" - headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + url = f"https://graph.microsoft.com/v1.0/users/{login}" if login else "https://graph.microsoft.com/v1.0/users" + headers = {"Authorization": f"Bearer {token}", "Accept": "application/json", "Content-Type": "application/json"} response = requests.get(url, headers=headers) if response.status_code == 200: users = response.json() - return [{"login": user["displayName"], "id": f'MS-{user["id"]}'} for user in users["value"]] + users = users.get("value", [users]) + return [{"login": user["displayName"], "id": user["id"], "pluginid": pluginid} for user in users] - return [ - {"id": "api-user-mock", "login": "mockuser", "pluginid": pluginid} - ] + return [] + + @security.private + @ram.cache(_cachekey_ms_users_inconsistent) + def queryMSApiUsersInconsistently(self, query="", properties=None): + pluginid = self.getId() + token = self._getMSAccessToken() + + customQuery = "" + + if not properties and query: + customQuery = f"displayName:{query}" + + if properties and properties.get("fullname"): + customQuery = f"displayName:{properties.get('fullname')}" + + elif properties and properties.get("email"): + customQuery = f"mail:{properties.get('email')}" + + if customQuery: + url = f'https://graph.microsoft.com/v1.0/users?$search="{customQuery}"' + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/json", + "Content-Type": "application/json", + "ConsistencyLevel": "eventual", + } + + response = requests.get(url, headers=headers) + + if response.status_code == 200: + users = response.json() + users = users.get("value", [users]) + return [{"login": user["displayName"], "id": user["id"], "pluginid": pluginid} for user in users] + + return [] + + @security.private + # @ram.cache(_cachekey_ms_users) + def queryMSApiUsersEndpoint(self, login="", exact=False, **properties): + if exact: + return self.queryMSApiUsers(login) + else: + return self.queryMSApiUsersInconsistently(login, properties) @security.private def enumerateUsers( @@ -280,14 +346,21 @@ def enumerateUsers( if id and login and id != login: raise ValueError("plugin does not support id different from login") search_id = id or login + from pprint import pprint + pprint({"search_id": search_id, + "kwargs": kw, "exact_match": exact_match, "sort_by": sort_by, "max_results": max_results}) + ret = list() + ret.extend(self.queryMSApiUsersEndpoint(search_id, exact_match, **kw)) + # if not search_id and not kw: + # api_users = self.queryMSApiUsers() + # pprint(api_users) + # return api_users if not search_id: - return () + return ret if not isinstance(search_id, str): raise NotImplementedError("sequence is not supported.") pluginid = self.getId() - ret = list() - # ret.extend(self.queryMSApiUsers(search_id)) # shortcut for exact match of login/id identity = None if exact_match and search_id and search_id in self._useridentities_by_userid: