diff --git a/src/DIRAC/FrameworkSystem/DB/AuthDB.py b/src/DIRAC/FrameworkSystem/DB/AuthDB.py deleted file mode 100644 index 2564ebe6580..00000000000 --- a/src/DIRAC/FrameworkSystem/DB/AuthDB.py +++ /dev/null @@ -1,345 +0,0 @@ -""" AuthDB class is a front-end to the AuthDB MySQL Database (via SQLAlchemy) -""" -import json -import time -import pprint - -from sqlalchemy import Column, Integer, Text, String -from sqlalchemy.orm import declarative_base, scoped_session -from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound - -from authlib.jose import KeySet, JsonWebKey -from authlib.common.security import generate_token - -from DIRAC import S_OK, S_ERROR -from DIRAC.Core.Base.SQLAlchemyDB import SQLAlchemyDB -from DIRAC.FrameworkSystem.private.authorization.utils.Tokens import OAuth2Token - - -Model = declarative_base() - - -class RefreshToken(Model): - __tablename__ = "RefreshToken" - __table_args__ = {"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4"} - jti = Column(String(255), nullable=False, primary_key=True) - issued_at = Column(Integer, nullable=False, default=0) - access_token = Column(Text, nullable=False) - refresh_token = Column(Text) - - -class JWK(Model): - __tablename__ = "JWK" - __table_args__ = {"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4"} - kid = Column(String(255), unique=True, primary_key=True, nullable=False) - key = Column(Text, nullable=False) - expires_at = Column(Integer, nullable=False, default=0) - - -class AuthSession(Model): - __tablename__ = "AuthSession" - __table_args__ = {"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4"} - id = Column(String(255), unique=True, primary_key=True, nullable=False) - uri = Column(String(255)) - state = Column(String(255)) - scope = Column(String(255)) - user_id = Column(String(255)) - username = Column(String(255)) - client_id = Column(String(255)) - user_code = Column(String(255)) - device_code = Column(String(255)) - interval = Column(Integer, nullable=False, default=5) - expires_at = Column(Integer, nullable=False, default=0) - expires_in = Column(Integer, nullable=False, default=0) - verification_uri = Column(String(255)) - verification_uri_complete = Column(String(255)) - - -class AuthDB(SQLAlchemyDB): - """AuthDB class is a front-end to the OAuth Database""" - - def __init__(self): - """Constructor""" - super().__init__() - self._initializeConnection("Framework/AuthDB") - result = self.__initializeDB() - if not result["OK"]: - raise Exception(f"Can't create tables: {result['Message']}") - self.session = scoped_session(self.sessionMaker_o) - - def __initializeDB(self): - """Create the tables""" - tablesInDB = self.inspector.get_table_names() - - # RefreshToken - if "RefreshToken" not in tablesInDB: - try: - RefreshToken.__table__.create(self.engine) # pylint: disable=no-member - except Exception as e: - return S_ERROR(e) - - # JWK - if "JWK" not in tablesInDB: - try: - JWK.__table__.create(self.engine) # pylint: disable=no-member - except Exception as e: - return S_ERROR(e) - - # AuthSession - if "AuthSession" not in tablesInDB: - try: - AuthSession.__table__.create(self.engine) # pylint: disable=no-member - except Exception as e: - return S_ERROR(e) - - return S_OK() - - def storeRefreshToken(self, token, tokenID=None): - """Store refresh token - - :param dict token: tokens as dict - :param str tokenID: token ID - - :return: S_OK(dict)/S_ERROR() - """ - iat = int(time.time()) - jti = tokenID or generate_token(10) - self.log.debug(f"Store {jti} token:\n", pprint.pformat(token)) - - session = self.session() - try: - session.add( - RefreshToken( - jti=jti, issued_at=iat, access_token=token["access_token"], refresh_token=token.get("refresh_token") - ) - ) - except Exception as e: - return self.__result(session, S_ERROR(f"Could not add refresh token: {repr(e)}")) - - self.log.info(f"Token with {jti} ID successfully added:\n", pprint.pformat(token)) - return S_OK(dict(jti=jti, iat=iat)) - - def revokeRefreshToken(self, tokenID): - """Revoke refresh token - - :param str tokenID: refresh token ID - - :return: S_OK()/S_ERROR() - """ - session = self.session() - try: - session.query(RefreshToken).filter(RefreshToken.jti == tokenID).delete() - except Exception as e: - return self.__result(session, S_ERROR(str(e))) - return S_OK() - - def getCredentialByRefreshToken(self, tokenID): - """Get refresh token credential - - :param str tokenID: refresh token ID - - :return: S_OK(dict)/S_ERROR() - """ - session = self.session() - try: - token = session.query(RefreshToken).filter(RefreshToken.jti == tokenID).first() - session.query(RefreshToken).filter(RefreshToken.jti == tokenID).delete() - except Exception as e: - return self.__result(session, S_ERROR(str(e))) - return self.__result(session, S_OK(OAuth2Token(self.__rowToDict(token)) if token else None)) - - def generateRSAKeys(self): - """Generate an RSA keypair with an exponent of 65537 in PEM format - - :return: S_OK/S_ERROR - """ - key = JsonWebKey.generate_key("RSA", 1024, is_private=True) - # as_dict has no arguments for authlib < 1.0.0 - # for authlib >= 1.0.0 - keyDict = dict( - key=json.dumps(key.as_dict(True)), kid=key.thumbprint(), expires_at=time.time() + (30 * 24 * 3600) - ) - session = self.session() - try: - session.add(JWK(**keyDict)) - except Exception as e: - return self.__result(session, S_ERROR(f"Could not generate keys: {e}")) - return self.__result(session, S_OK(keyDict)) - - def getKeySet(self): - """Get key set - - :return: S_OK(obj)/S_ERROR() - """ - result = self.getActiveKeys() - if result["OK"] and not result["Value"]: - result = self.generateRSAKeys() - if result["OK"]: - result["Value"] = [result["Value"]] - if not result["OK"]: - return result - return S_OK(KeySet([JsonWebKey.import_key(json.loads(key["key"])) for key in result["Value"]])) - - def getPrivateKey(self, kid=None): - """Get private key - - :param str kid: key ID - - :return: S_OK(obj)/S_ERROR() - """ - result = self.getActiveKeys(kid) - if not result["OK"]: - return result - jwks = result["Value"] - if kid: - strkey = jwks[0]["key"] - return S_OK(JsonWebKey.import_key(json.loads(jwks[0]["key"]))) - newer = {} - for jwk in jwks: - if int(jwk["expires_at"]) > int(newer.get("expires_at", time.time() + (24 * 3600))): - newer = jwk - if not newer.get("key"): - result = self.generateRSAKeys() - if not result["OK"]: - return result - newer = result["Value"] - return S_OK(JsonWebKey.import_key(json.loads(newer["key"]))) - - def getActiveKeys(self, kid=None): - """Get active keys - - :param str kid: key ID - - :return: S_OK(list)/S_ERROR() - """ - session = self.session() - try: - # Remove all expired jwks - session.query(JWK).filter(JWK.expires_at < time.time()).delete() - jwks = session.query(JWK).filter(JWK.expires_at > time.time()).all() - if kid: - jwks = [jwk for jwk in jwks if jwk.kid == kid] - except NoResultFound: - return self.__result(session, S_OK([])) - except Exception as e: - return self.__result(session, S_ERROR(str(e))) - return self.__result(session, S_OK([self.__rowToDict(jwk) for jwk in jwks])) - - def removeKeys(self): - """Get active keys - - :return: S_OK(list)/S_ERROR() - """ - session = self.session() - try: - session.query(JWK).delete() - except Exception as e: - return self.__result(session, S_ERROR(str(e))) - return self.__result(session, S_OK()) - - def addSession(self, data): - """Add new session - - :param dict data: session metadata - - :return: S_OK(dict)/S_ERROR() - """ - attrts = {} - if not data.get("expires_at"): - data["expires_at"] = data["expires_in"] + time.time() - self.log.debug("Add authorization session:", data) - for k, v in data.items(): - if k not in AuthSession.__dict__.keys(): - self.log.warn(f"{k} is not expected as authentication session attribute.") - else: - attrts[k] = v - session = self.session() - try: - session.add(AuthSession(**attrts)) - except Exception as e: - return self.__result(session, S_ERROR(f"Could not add Token: {e}")) - return self.__result(session, S_OK("Token successfully added")) - - def updateSession(self, data, sessionID): - """Update session data - - :param dict data: data info - :param str sessionID: sessionID - - :return: S_OK(object)/S_ERROR() - """ - self.removeSession(sessionID=sessionID) - return self.addSession(data) - - def removeSession(self, sessionID): - """Remove session - - :param str sessionID: session id - - :return: S_OK()/S_ERROR() - """ - session = self.session() - try: - # Remove all expired sessions - session.query(AuthSession).filter(AuthSession.expires_at < time.time()).delete() - session.query(AuthSession).filter(AuthSession.id == sessionID).delete() - except Exception as e: - return self.__result(session, S_ERROR(str(e))) - return self.__result(session, S_OK()) - - def getSession(self, sessionID): - """Get client - - :param str sessionID: session id - - :return: S_OK(dict)/S_ERROR() - """ - session = self.session() - try: - resData = session.query(AuthSession).filter(AuthSession.id == sessionID).first() - except MultipleResultsFound: - return self.__result(session, S_ERROR(f"{sessionID} is not unique ID.")) - except NoResultFound: - return self.__result(session, S_ERROR(f"{sessionID} session is expired.")) - except Exception as e: - return self.__result(session, S_ERROR(str(e))) - return self.__result(session, S_OK(self.__rowToDict(resData))) - - def getSessionByUserCode(self, userCode): - """Get client - - :param str userCode: user code - - :return: S_OK(dict)/S_ERROR() - """ - session = self.session() - try: - resData = session.query(AuthSession).filter(AuthSession.user_code == userCode).first() - except MultipleResultsFound: - return self.__result(session, S_ERROR(f"{userCode} is not unique ID.")) - except NoResultFound: - return self.__result(session, S_ERROR(f"Session for {userCode} user code is expired.")) - except Exception as e: - return self.__result(session, S_ERROR(str(e))) - return self.__result(session, S_OK(self.__rowToDict(resData))) - - def __result(self, session, result=None): - try: - if not result["OK"]: - session.rollback() - else: - session.commit() - except Exception as e: - session.rollback() - result = S_ERROR(f"Could not commit: {e}") - session.close() - return result - - def __rowToDict(self, row): - """Convert sqlalchemy row to dictionary - - :param object row: sqlalchemy row - - :return: dict - """ - return {c.name: str(getattr(row, c.name)) for c in row.__table__.columns} if row else {} diff --git a/src/DIRAC/FrameworkSystem/DB/AuthDB.sql b/src/DIRAC/FrameworkSystem/DB/AuthDB.sql deleted file mode 100644 index 75f4e1577cb..00000000000 --- a/src/DIRAC/FrameworkSystem/DB/AuthDB.sql +++ /dev/null @@ -1,2 +0,0 @@ -# Everything is created by the DB object upon instantiation if it does not exists. -use AuthDB; diff --git a/src/DIRAC/FrameworkSystem/private/authorization/AuthServer.py b/src/DIRAC/FrameworkSystem/private/authorization/AuthServer.py deleted file mode 100644 index 33145d62c48..00000000000 --- a/src/DIRAC/FrameworkSystem/private/authorization/AuthServer.py +++ /dev/null @@ -1,549 +0,0 @@ -""" This class provides authorization server activity. """ -import re -import json -import pprint -from dominate import tags as dom - -from authlib.jose import JsonWebKey, jwt -from authlib.oauth2 import AuthorizationServer as _AuthorizationServer -from authlib.oauth2.base import OAuth2Error -from authlib.oauth2.rfc7636 import CodeChallenge -from authlib.oauth2.rfc6749.util import scope_to_list, list_to_scope - -from DIRAC import gLogger, S_OK, S_ERROR, gConfig -from DIRAC.FrameworkSystem.DB.AuthDB import AuthDB -from DIRAC.Resources.IdProvider.Utilities import getIdProviderIdentifiers -from DIRAC.Resources.IdProvider.IdProviderFactory import IdProviderFactory -from DIRAC.ConfigurationSystem.Client.Utilities import isDownloadProxyAllowed -from DIRAC.ConfigurationSystem.Client.Helpers.Registry import ( - getUsernameForDN, - getEmailsForGroup, - wrapIDAsDN, - getDNForUsername, - getIdPForGroup, - getGroupOption, -) -from DIRAC.FrameworkSystem.Client.ProxyManagerClient import ProxyManagerClient -from DIRAC.FrameworkSystem.Client.TokenManagerClient import TokenManagerClient -from DIRAC.Core.Tornado.Server.private.BaseRequestHandler import TornadoResponse -from DIRAC.FrameworkSystem.private.authorization.utils.Clients import getDIRACClients, Client -from DIRAC.FrameworkSystem.private.authorization.utils.Requests import OAuth2Request, createOAuth2Request -from DIRAC.FrameworkSystem.private.authorization.utils.Utilities import collectMetadata, getHTML -from DIRAC.FrameworkSystem.private.authorization.grants.RevokeToken import RevocationEndpoint -from DIRAC.FrameworkSystem.private.authorization.grants.RefreshToken import RefreshTokenGrant -from DIRAC.FrameworkSystem.private.authorization.grants.DeviceFlow import DeviceAuthorizationEndpoint, DeviceCodeGrant -from DIRAC.FrameworkSystem.private.authorization.grants.AuthorizationCode import AuthorizationCodeGrant - -try: - from authlib.oauth2 import JsonRequest -except ImportError: - from authlib.oauth2 import HttpRequest as JsonRequest - -sLog = gLogger.getSubLogger(__name__) - - -class AuthServer(_AuthorizationServer): - """Implementation of the :class:`authlib.oauth2.rfc6749.AuthorizationServer`. - - This framework has been changed and simplified to be used for DIRAC purposes, - namely authorization on the third party side and saving the received extended - long-term access tokens on the DIRAC side with the possibility of their future - use on behalf of the user without his participation. - - The idea is that DIRAC itself is not an identity provider and relies on third-party - resources such as EGI Checkin or WLCG IAM. - - Initialize:: - - server = AuthServer() - """ - - LOCATION = None - - def __init__(self): - self.db = AuthDB() # place to store session information - self.log = sLog - self.idps = IdProviderFactory() - self.proxyCli = ProxyManagerClient() # take care about proxies - self.tokenCli = TokenManagerClient() # take care about tokens - self._tokenDB = None - # The authorization server has its own settings, but they are standardized - self.metadata = collectMetadata() - self.metadata.validate() - # Initialize AuthorizationServer - _AuthorizationServer.__init__(self, scopes_supported=self.metadata["scopes_supported"]) - # authlib requires the following methods: - # The following `save_token` method is called when requesting a new access token to save it after it is generated. - # Let's skip this step, because getting tokens and saving them if necessary has already taken place in `generate_token` method. - self.save_token = lambda x, y: None - # Framework integration can re-implement this method to support signal system. - # But in this implementation, this system is not used. - self.send_signal = lambda *x, **y: None - # The main method that will return an access token to the user (this can be a proxy) - self.generate_token = self.generateProxyOrToken - # Register configured grants - self.register_grant(RefreshTokenGrant) # Enable refreshing tokens - # Enable device code flow - self.register_grant(DeviceCodeGrant) - self.register_endpoint(DeviceAuthorizationEndpoint) - self.register_endpoint(RevocationEndpoint) # Enable revokation tokens - self.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=True)]) # Enable authorization code flow - - @property - def tokenDB(self): - """Lazy initialization of TokenDB""" - if self._tokenDB is None: - from DIRAC.FrameworkSystem.DB.TokenDB import TokenDB - - self._tokenDB = TokenDB(parentLogger=self.log) - return self._tokenDB - - # pylint: disable=method-hidden - def query_client(self, client_id): - """Search authorization client. - - :param str clientID: client ID - - :return: client as object or None - """ - gLogger.debug(f"Try to query {client_id} client") - clients = getDIRACClients() - for cli in clients: - if client_id == clients[cli]["client_id"]: - gLogger.debug(f"Found {cli} client:\n", pprint.pformat(clients[cli])) - # Authorization successful - return Client(clients[cli]) - # Authorization failed, client not found - return None - - def _getScope(self, scope, param): - """Get parameter scope - - :param str scope: scope - :param str param: parameter scope - - :return: str or None - """ - try: - return [s.split(":")[1] for s in scope_to_list(scope) if s.startswith(f"{param}:") and s.split(":")[1]][0] - except Exception: - return None - - def generateProxyOrToken( - self, client, grant_type, user=None, scope=None, expires_in=None, include_refresh_token=True - ): - """Generate proxy or tokens after authorization - - :param client: instance of the IdP client - :param grant_type: authorization grant type (unused) - :param str user: user identifier - :param str scope: requested scope - :param expires_in: when the token should expire (unused) - :param bool include_refresh_token: to include refresh token (unused) - - :return: dict or str -- will return tokens as dict or proxy as string - """ - # Read requested scopes - group = self._getScope(scope, "g") - lifetime = self._getScope(scope, "lifetime") - # Found provider name for group - provider = getIdPForGroup(group) - - # Search DIRAC username by user ID - result = getUsernameForDN(wrapIDAsDN(user)) - if not result["OK"]: - raise OAuth2Error(result["Message"]) - userName = result["Value"] - - # User request a proxy - if "proxy" in scope_to_list(scope): - # Try to return user proxy if proxy scope present in the authorization request - if not isDownloadProxyAllowed(): - raise OAuth2Error("You can't get proxy, configuration(allowProxyDownload) not allow to do that.") - sLog.debug(f"Try to query {user}@{group} proxy{f'with lifetime:{lifetime}' if lifetime else ''}") - # Get user DNs - result = getDNForUsername(userName) - if not result["OK"]: - raise OAuth2Error(result["Message"]) - userDNs = result["Value"] - err = [] - # Try every DN to generate a proxy - for dn in userDNs: - sLog.debug(f"Try to get proxy for {dn}") - params = {} - if lifetime: - params["requiredTimeLeft"] = int(lifetime) - # if the configuration describes adding a VOMS extension, we will do so - if getGroupOption(group, "AutoAddVOMS", False): - result = self.proxyCli.downloadVOMSProxy(dn, group, **params) - else: - # otherwise we will return the usual proxy - result = self.proxyCli.downloadProxy(dn, group, **params) - if not result["OK"]: - err.append(result["Message"]) - else: - sLog.info("Proxy was created.") - result = result["Value"].dumpAllToString() - if not result["OK"]: - raise OAuth2Error(result["Message"]) - # Proxy generated - return { - "proxy": result["Value"].decode() if isinstance(result["Value"], bytes) else result["Value"] - } - # Proxy cannot be generated or not found - raise OAuth2Error("; ".join(err)) - - # User request a tokens - else: - # Ask TokenManager to generate new tokens for user - result = self.tokenCli.getToken(userName, group) - if not result["OK"]: - raise OAuth2Error(result["Message"]) - token = result["Value"] - # Wrap the refresh token and register it to protect against reuse - result = self.registerRefreshToken( - dict(sub=user, scope=scope, provider=provider, azp=client.get_client_id()), token - ) - if not result["OK"]: - raise OAuth2Error(result["Message"]) - # Return tokens as dictionary - return result["Value"] - - def __signToken(self, payload): - """Sign token - - :param dict payload: payload - - :return: S_OK(str)/S_ERROR() - """ - result = self.db.getPrivateKey() - if not result["OK"]: - return result - key = result["Value"] - try: - return S_OK(jwt.encode(dict(alg="RS256", kid=key.thumbprint()), payload, key).decode("utf-8")) - except Exception as e: - sLog.exception(e) - return S_ERROR(repr(e)) - - def readToken(self, token): - """Decode self token - - :param str token: token to decode - - :return: S_OK(dict)/S_ERROR() - """ - result = self.db.getKeySet() - if not result["OK"]: - return result - try: - return S_OK(jwt.decode(token, JsonWebKey.import_key_set(result["Value"].as_dict()))) - except Exception as e: - sLog.exception(e) - return S_ERROR(repr(e)) - - def registerRefreshToken(self, payload, token): - """Register refresh token to protect it from reuse - - :param dict payload: payload - :param dict token: token as a dictionary - - :return: S_OK(dict)S_ERROR() - """ - result = self.db.storeRefreshToken(token, payload.get("jti")) - if result["OK"]: - payload.update(result["Value"]) - result = self.__signToken(payload) - if not result["OK"]: - if token.get("refresh_token"): - prov = self.idps.getIdProvider(payload["provider"]) - if prov["OK"]: - prov["Value"].revokeToken(token["refresh_token"]) - prov["Value"].revokeToken(token["access_token"], "access_token") - return result - token["refresh_token"] = result["Value"] - return S_OK(token) - - def getIdPAuthorization(self, provider, request): - """Submit subsession to authorize with chosen provider and return dict with authorization url and session number - - :param str provider: provider name - :param object request: main session request - - :return: S_OK(response)/S_ERROR() -- dictionary contain response generated by `handle_response` - """ - result = self.idps.getIdProvider(provider) - if not result["OK"]: - raise Exception(result["Message"]) - idpObj = result["Value"] - authURL, state, session = idpObj.submitNewSession() - session["state"] = state - session["Provider"] = provider - session["firstRequest"] = request if isinstance(request, dict) else request.toDict() - - sLog.verbose("Redirect to", authURL) - return self.handle_response(302, {}, [("Location", authURL)], session) - - def parseIdPAuthorizationResponse(self, response, session): - """Fill session by user profile, tokens, comment, OIDC authorize status, etc. - Prepare dict with user parameters, if DN is absent there try to get it. - Create new or modify existing DIRAC user and store the session - - :param dict response: authorization response - :param str session: session - - :return: S_OK(dict)/S_ERROR() - """ - providerName = session.pop("Provider") - sLog.debug(f"Try to parse authentication response from {providerName}:\n", pprint.pformat(response)) - # Parse response - result = self.idps.getIdProvider(providerName) - if not result["OK"]: - return result - idpObj = result["Value"] - result = idpObj.parseAuthResponse(response, session) - if not result["OK"]: - return result - - # FINISHING with IdP - # As a result of authentication we will receive user credential dictionary - credDict, payload = result["Value"] - - sLog.debug("Read profile:", pprint.pformat(credDict)) - # Is ID registered? - result = getUsernameForDN(credDict["DN"]) - if not result["OK"]: - comment = f"ID {credDict['ID']} is not registered in DIRAC. " - payload.update(idpObj.getUserProfile(idpObj.token.get("access_token")).get("Value", {})) - result = self.__registerNewUser(providerName, payload) - - if result["OK"]: - comment += "Administrators have been notified about you." - else: - comment += "Please, contact the DIRAC administrators." - - # Notify user about problem - html = getHTML("unregistered user!", info=comment, theme="warning") - return S_ERROR(html) - - credDict["username"] = result["Value"] - - # Update token for user. This token will be stored separately in the database and - # updated from time to time. This token will never be transmitted, - # it will be used to make exchange token requests. - result = self.tokenDB.updateToken(idpObj.token, credDict["ID"], idpObj.name, rt_expired_in=24 * 3600) - if not result["OK"]: - return result - # Revoke old tokens - for oldToken in result["Value"]: - if "refresh_token" in oldToken and oldToken["refresh_token"] != idpObj.token.get("refresh_token"): - idpObj.revokeToken(oldToken["refresh_token"]) - return S_OK(credDict) - - def create_oauth2_request(self, request, method_cls=OAuth2Request, use_json=False): - """Parse request. Rewrite authlib method.""" - self.log.debug("Create OAuth2 request", "with json" if use_json else "") - return createOAuth2Request(request, method_cls, use_json) - - def create_json_request(self, request): - """Parse request. Rewrite authlib method.""" - return self.create_oauth2_request(request, JsonRequest, True) - - def validate_requested_scope(self, scope): - """See :func:`authlib.oauth2.rfc6749.authorization_server.validate_requested_scope`""" - # We also consider parametric scope containing ":" charter - extended_scope = list_to_scope( - [re.sub(r":.*$", ":", s) for s in scope_to_list((scope or "").replace("+", " "))] - ) - super().validate_requested_scope(extended_scope) - - def handle_response(self, status_code=None, payload=None, headers=None, newSession=None, delSession=None): - """Handle response - - :param int status_code: http status code - :param payload: response payload - :param list headers: headers - :param dict newSession: session data to store - - :return: TornadoResponse() - """ - resp = TornadoResponse(payload, status_code) - if not isinstance(payload, dict): - sLog.debug( - f"Handle authorization response with {status_code} status code:", - "HTML page" if payload.startswith("") else payload, - ) - elif "error" in payload: - resp.clear_cookie("auth_session") # pylint: disable=no-member - sLog.error(f"{payload['error']}: {payload.get('error_description', 'unknown')}") - if headers: - sLog.debug("Headers:", headers) - for key, value in headers: - resp.set_header(key, value) # pylint: disable=no-member - if newSession: - sLog.debug("Initialize new session:", newSession) - # pylint: disable=no-member - resp.set_secure_cookie("auth_session", json.dumps(newSession), secure=True, httponly=True) - if delSession: - resp.clear_cookie("auth_session") # pylint: disable=no-member - return resp - - def create_authorization_response(self, response, username): - """Rewrite original Authlib method - `authlib.authlib.oauth2.rfc6749.authorization_server.create_authorization_response` - to catch errors and remove authorization session. - - :return: TornadoResponse object - """ - try: - response = super().create_authorization_response(response, username) - response.clear_cookie("auth_session") # pylint: disable=no-member - return response - except Exception as e: - sLog.exception(e) - return self.handle_response( - payload=getHTML("server error", theme="error", body="traceback", info=repr(e)), delSession=True - ) - - def validate_consent_request(self, request, provider=None): - """Validate current HTTP request for authorization page. This page - is designed for resource owner to grant or deny the authorization:: - - :param object request: tornado request - :param provider: provider - - :return: response generated by `handle_response` or S_ERROR or html - """ - try: - request = self.create_oauth2_request(request) - # Check Identity Provider - req = self.validateIdentityProvider(request, provider) - - # If return HTML page with IdP selector - if isinstance(req, str): - return req - - sLog.info("Validate consent request for ", req.state) - grant = self.get_authorization_grant(req) - sLog.debug("Use grant:", grant.GRANT_TYPE) - grant.validate_consent_request() - if not hasattr(grant, "prompt"): - grant.prompt = None - - # Submit second auth flow through IdP - return self.getIdPAuthorization(req.provider, req) - - except OAuth2Error as error: - self.db.removeSession(request.sessionID) - code, body, _ = error(None) - return self.handle_response( - payload=getHTML(repr(error), state=code, body=body, info="OAuth2 error."), delSession=True - ) - except Exception as e: - self.db.removeSession(request.sessionID) - sLog.exception(e) - return self.handle_response( - payload=getHTML("server error", theme="error", body="traceback", info=repr(e)), delSession=True - ) - - def validateIdentityProvider(self, request, provider): - """Check if identity provider registered in DIRAC - - :param object request: request - :param str provider: provider name - - :return: OAuth2Request object or HTML -- new request with provider name or provider selector - """ - if provider: - request.provider = provider - - # Find identity provider for group - groupProvider = getIdPForGroup(request.group) if request.groups else None - - # If access token is requested for a group that is not registered in any identity provider - # or the requested provider does not match the group, return error - if request.group and not groupProvider and "proxy" not in request.scope: - raise Exception(f"The {request.group} group belongs to the VO that is not tied to any Identity Provider.") - - sLog.debug(f"Check if {request.provider} identity provider registered in DIRAC...") - # Research supported IdPs - result = getIdProviderIdentifiers() - if not result["OK"]: - raise Exception(result["Message"]) - - idPs = result["Value"] - if not idPs: - raise Exception("No identity providers found.") - - if request.provider: - if request.provider not in idPs: - raise Exception(f"{request.provider} identity provider is not registered.") - elif groupProvider and request.provider != groupProvider: - raise Exception( - 'The %s group Identity Provider is "%s" and not "%s".' - % (request.group, groupProvider, request.provider) - ) - return request - - # If no identity provider is specified, it must be assigned - if groupProvider: - request.provider = groupProvider - return request - - # If only one identity provider is registered, then choose it - if len(idPs) == 1: - request.provider = idPs[0] - return request - - # Choose IdP HTML interface - with dom.div(cls="row m-5 justify-content-md-center") as tag: - for idP in idPs: - result = gConfig.getOptionsDict(f"/Resources/IdProviders/{idP}") - if result["OK"]: - logo = result["Value"].get("logoURL") - with dom.div(cls="col-md-6 p-2").add(dom.div(cls="card shadow-lg h-100 border-0")): - with dom.div(cls="row m-2 justify-content-md-center align-items-center h-100"): - with dom.div(cls="col-auto"): - dom.h2(idP) - dom.a( - href=f"{self.LOCATION}/authorization/{idP}?{request.query}", - cls="stretched-link", - ) - if logo: - dom.div(dom.img(src=logo, cls="card-img"), cls="col-auto") - - # Render into HTML - return getHTML( - "Identity Provider selection..", - body=tag, - icon="fingerprint", - info="Dirac itself is not an Identity Provider. " "You will need to select one to continue.", - ) - - def __registerNewUser(self, provider, payload): - """Register new user - - :param str provider: provider - :param dict payload: user information dictionary - - :return: S_OK()/S_ERROR() - """ - from DIRAC.FrameworkSystem.Client.NotificationClient import NotificationClient - - username = payload["sub"] - - mail = {} - mail["subject"] = f"[DIRAC AS] User {username} to be added." - mail["body"] = f"User {username} was authenticated by {provider}." - mail["body"] += f"\n\nNew user to be added with the following information:\n{pprint.pformat(payload)}" - mail["body"] += f"\n\nPlease, add '{wrapIDAsDN(username)}' to /Register/Users//DN option.\n" - mail["body"] += "\n\n------" - mail["body"] += "\n This is a notification from the DIRAC authorization service, please do not reply.\n" - result = S_OK() - for addresses in getEmailsForGroup("dirac_admin"): - result = NotificationClient().sendMail(addresses, mail["subject"], mail["body"], localAttempt=False) - if not result["OK"]: - sLog.error(result["Message"]) - if result["OK"]: - sLog.info(result["Value"], "administrators have been notified about a new user.") - return result diff --git a/src/DIRAC/FrameworkSystem/private/authorization/grants/AuthorizationCode.py b/src/DIRAC/FrameworkSystem/private/authorization/grants/AuthorizationCode.py deleted file mode 100644 index 611719f499d..00000000000 --- a/src/DIRAC/FrameworkSystem/private/authorization/grants/AuthorizationCode.py +++ /dev/null @@ -1,119 +0,0 @@ -""" This class describe Authorization Code grant type -""" -from time import time -from authlib.jose import JsonWebSignature -from authlib.oauth2.base import OAuth2Error -from authlib.oauth2.rfc6749.grants import AuthorizationCodeGrant as _AuthorizationCodeGrant -from authlib.common.encoding import json_b64encode, urlsafe_b64decode, json_loads - - -class OAuth2Code(dict): - """This class describes Authorization Code object""" - - def __init__(self, params): - """C'or""" - params["auth_time"] = params.get("auth_time", int(time())) - super().__init__(params) - - @property - def user(self): - return self.get("user_id") - - @property - def code_challenge(self): - return self.get("code_challenge") - - @property - def code_challenge_method(self): - return self.get("code_challenge_method", "pain") - - def is_expired(self): - return self.get("auth_time") + 300 < time() - - def get_redirect_uri(self): - return self.get("redirect_uri") - - def get_scope(self): - return self.get("scope", "") - - def get_auth_time(self): - return self.get("auth_time") - - def get_nonce(self): - return self.get("nonce") - - -class AuthorizationCodeGrant(_AuthorizationCodeGrant): - """See :class:`authlib.oauth2.rfc6749.grants.AuthorizationCodeGrant`""" - - TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post", "none"] - - def save_authorization_code(self, code, request): - pass - - def delete_authorization_code(self, authorization_code): - pass - - def query_authorization_code(self, code, client): - """Parse authorization code - - :param code: authorization code as JWS - :param client: client - - :return: OAuth2Code or None - """ - self.server.log.debug("Query authorization code:", code) - jws = JsonWebSignature(algorithms=["RS256"]) - result = self.server.db.getKeySet() - if not result["OK"]: - raise Exception(result["Message"]) - err = None - data = None - for key in result["Value"].keys: - try: - data = jws.deserialize_compact(code, key) - except Exception as e: - err = e - if err: - self.server.log.error("Cannot get authorization code:", repr(err)) - return None - try: - item = OAuth2Code(json_loads(urlsafe_b64decode(data["payload"]))) - self.server.log.debug("Authorization code scope:", item.get_scope()) - except Exception as e: - self.server.log.error("Cannot read authorization code:", repr(e)) - return None - if not item.is_expired(): - return item - - def authenticate_user(self, authorization_code): - """Authenticate the user related to this authorization_code. - - :param authorization_code: authorization code - """ - return authorization_code.user - - def generate_authorization_code(self): - """The method to generate "code" value for authorization code data. - - :return: str - """ - self.server.log.debug("Generate authorization code for credentials:", self.request.user) - jws = JsonWebSignature(algorithms=["RS256"]) - protected = {"alg": "RS256"} - code = OAuth2Code( - { - "user_id": self.request.user["ID"], - # These scope already contain DIRAC groups - "scope": self.request.data["scope"], - "redirect_uri": self.request.args["redirect_uri"], - "client_id": self.request.args["client_id"], - "code_challenge": self.request.args.get("code_challenge"), - "code_challenge_method": self.request.args.get("code_challenge_method"), - } - ) - self.server.log.debug("Authorization code generated:", dict(code)) - result = self.server.db.getPrivateKey() - if not result["OK"]: - raise OAuth2Error(f"Cannot generate authorization code: {result['Message']}") - return jws.serialize_compact(protected, json_b64encode(dict(code)), result["Value"]) diff --git a/src/DIRAC/FrameworkSystem/private/authorization/grants/DeviceFlow.py b/src/DIRAC/FrameworkSystem/private/authorization/grants/DeviceFlow.py deleted file mode 100644 index f357236e7f7..00000000000 --- a/src/DIRAC/FrameworkSystem/private/authorization/grants/DeviceFlow.py +++ /dev/null @@ -1,158 +0,0 @@ -import time -from dominate import document, tags as dom -from authlib.oauth2 import OAuth2Error -from authlib.oauth2.rfc6749.grants import AuthorizationEndpointMixin -from authlib.oauth2.rfc6749.errors import InvalidClientError, UnauthorizedClientError -from authlib.oauth2.rfc8628 import ( - DeviceAuthorizationEndpoint as _DeviceAuthorizationEndpoint, - DeviceCodeGrant as _DeviceCodeGrant, - DeviceCredentialDict, -) -from DIRAC.FrameworkSystem.private.authorization.utils.Utilities import getHTML - - -class DeviceAuthorizationEndpoint(_DeviceAuthorizationEndpoint): - """See :class:`authlib.oauth2.rfc8628.DeviceAuthorizationEndpoint`""" - - def create_endpoint_response(self, req): - """See :func:`authlib.oauth2.rfc8628.DeviceAuthorizationEndpoint.create_endpoint_response`""" - # Share original request object to endpoint class before create_endpoint_response - self.req = req - return super().create_endpoint_response(req) - - def get_verification_uri(self): - """Create verification uri when `DeviceCode` flow initialized - - :return: str - """ - return self.server.metadata["device_authorization_endpoint"] - - def save_device_credential(self, client_id, scope, data): - """Save device credentials - - :param str client_id: client id - :param str scope: request scopes - :param dict data: device credentials - """ - data.update( - dict( - uri="{api}?{query}&response_type=device&client_id={client_id}&scope={scope}".format( - api=data["verification_uri"], - query=self.req.query, - client_id=client_id, - scope=scope, - ), - id=data["device_code"], - client_id=client_id, - scope=scope, - ) - ) - result = self.server.db.addSession(data) - if not result["OK"]: - raise OAuth2Error("Cannot save device credentials", result["Message"]) - - -class DeviceCodeGrant(_DeviceCodeGrant, AuthorizationEndpointMixin): - """See :class:`authlib.oauth2.rfc8628.DeviceCodeGrant`""" - - RESPONSE_TYPES = {"device"} - - def validate_authorization_request(self): - """Validate authorization request - - :return: None - """ - # Validate client for this request - client_id = self.request.client_id - self.server.log.debug("Validate authorization request of", client_id) - if client_id is None: - raise InvalidClientError(state=self.request.state) - client = self.server.query_client(client_id) - if not client: - raise InvalidClientError(state=self.request.state) - response_type = self.request.response_type - if not client.check_response_type(response_type): - raise UnauthorizedClientError(f'The client is not authorized to use "response_type={response_type}"') - self.request.client = client - self.validate_requested_scope() - - # Check user_code, when user go to authorization endpoint - userCode = self.request.args.get("user_code") - if not userCode: - raise OAuth2Error("user_code is absent.") - - # Get session from cookie - if not self.server.db.getSessionByUserCode(userCode): - raise OAuth2Error(f"Session with {userCode} user code is expired.") - return None - - def create_authorization_response(self, redirect_uri, user): - """Mark session as authed with received user - - :param str redirect_uri: redirect uri - :param dict user: dictionary with username and userID - - :return: result of `handle_response` - """ - result = self.server.db.getSessionByUserCode(self.request.data["user_code"]) - if not result["OK"]: - return 500, getHTML( - "server error", - theme="error", - body=result["Message"], - info=f"Failed to read {self.request.data['user_code']} authorization session.", - ) - data = result["Value"] - data.update(dict(user_id=user["ID"], uri=self.request.uri, username=user["username"], scope=self.request.scope)) - # Save session with user - result = self.server.db.updateSession(data, data["id"]) - if not result["OK"]: - return 500, getHTML( - "server error", - theme="error", - body=result["Message"], - info=f"Failed to save {self.request.data['user_code']} authorization session status.", - ) - - # Notify user that authorization completed. - return 200, getHTML( - "authorization complete!", - theme="success", - info="Authorization has been completed, now you can close this window and return to the terminal.", - ) - - def query_device_credential(self, device_code): - """Get device credential from previously savings via ``DeviceAuthorizationEndpoint``. - - :param str device_code: device code - - :return: dict - """ - result = self.server.db.getSession(device_code) - if not result["OK"]: - raise OAuth2Error(result["Message"]) - data = result["Value"] - if not data: - return None - data["verification_uri"] = self.server.metadata["device_authorization_endpoint"] - data["expires_at"] = int(data["expires_in"]) + int(time.time()) - data["interval"] = DeviceAuthorizationEndpoint.INTERVAL - return DeviceCredentialDict(data) - - def query_user_grant(self, user_code): - """Check if user alredy authed and return it to token generator - - :param str user_code: user code - - :return: (str, bool) or None -- user dict and user auth status - """ - result = self.server.db.getSessionByUserCode(user_code) - if not result["OK"]: - raise OAuth2Error("Cannot found authorization session", result["Message"]) - return (result["Value"]["user_id"], True) if result["Value"].get("username", "None") != "None" else None - - def should_slow_down(self, *args): - """The authorization request is still pending and polling should continue, - but the interval MUST be increased by 5 seconds for this and all subsequent requests. - """ - return False diff --git a/src/DIRAC/FrameworkSystem/private/authorization/grants/RefreshToken.py b/src/DIRAC/FrameworkSystem/private/authorization/grants/RefreshToken.py deleted file mode 100644 index 46aa8a0fef1..00000000000 --- a/src/DIRAC/FrameworkSystem/private/authorization/grants/RefreshToken.py +++ /dev/null @@ -1,75 +0,0 @@ -from authlib.oauth2.base import OAuth2Error -from authlib.oauth2.rfc6749.grants import RefreshTokenGrant as _RefreshTokenGrant - -from DIRAC.ConfigurationSystem.Client.Helpers.Registry import getUsernameForDN, wrapIDAsDN - - -class RefreshTokenGrant(_RefreshTokenGrant): - """See :class:`authlib.oauth2.rfc6749.grants.RefreshTokenGrant`""" - - DEFAULT_EXPIRES_AT = 12 * 3600 - TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post", "none"] - - def authenticate_refresh_token(self, refresh_token): - """Get credential for token - - :param str refresh_token: refresh token - - :return: dict or None - """ - result = self.server.readToken(refresh_token) - if not result["OK"]: - raise OAuth2Error(result["Message"]) - rtDict = result["Value"] - result = self.server.db.getCredentialByRefreshToken(rtDict["jti"]) - if not result["OK"]: - raise OAuth2Error(result["Message"]) - credential = result["Value"] - - if int(rtDict["iat"]) != int(credential["issued_at"]): - # An attempt to reuse the refresh token was detected - prov = self.server.idps.getIdProvider(rtDict["provider"]) - if prov["OK"]: - prov["Value"].revokeToken(credential["refresh_token"]) - prov["Value"].revokeToken(credential["access_token"], "access_token") - return None - - credential.update(rtDict) - return credential - - def authenticate_user(self, credential): - """Authorize user - - :param dict credential: credential (token payload) - - :return: str or bool - """ - result = getUsernameForDN(wrapIDAsDN(credential["sub"])) - if not result["OK"]: - self.server.log.error(result["Message"]) - return result.get("Value") - - def issue_token(self, user, credential): - """Refresh tokens - - :param user: unuse - :param dict credential: token credential - - :return: dict - """ - if credential["refresh_token"]: - result = self.server.idps.getIdProvider(credential["provider"]) - if result["OK"]: - result = result["Value"].refreshToken(credential["refresh_token"]) - else: - result = self.server.tokenCli.getToken(user, self.server._getScope(credential["scope"], "g")) - if result["OK"]: - token = result["Value"] - result = self.server.registerRefreshToken(credential, token) - if not result["OK"]: - raise OAuth2Error(result["Message"]) - return result["Value"] - - def revoke_old_credential(self, credential): - """Remove old credential""" - pass diff --git a/src/DIRAC/FrameworkSystem/private/authorization/grants/RevokeToken.py b/src/DIRAC/FrameworkSystem/private/authorization/grants/RevokeToken.py deleted file mode 100644 index f11b122f23a..00000000000 --- a/src/DIRAC/FrameworkSystem/private/authorization/grants/RevokeToken.py +++ /dev/null @@ -1,42 +0,0 @@ -from authlib.oauth2.base import OAuth2Error -from authlib.oauth2.rfc7009 import RevocationEndpoint as _RevocationEndpoint - - -class RevocationEndpoint(_RevocationEndpoint): - """See :class:`authlib.oauth2.rfc7009.RevocationEndpoint`""" - - def query_token(self, token, token_type_hint, client): - """Query requested token from database. - - :param str token: token - :param str token_type_hint: token type - :param client: client - - :return: dict - """ - if token_type_hint == "refresh_token": # nosec: B105 - result = self.server.readToken(token) - if not result["OK"]: - raise OAuth2Error(result["Message"]) - rtDict = result["Value"] - result = self.server.db.getCredentialByRefreshToken(rtDict["jti"]) - if not result["OK"]: - raise OAuth2Error(result["Message"]) - return result["Value"] - return {token_type_hint: token} - - def revoke_token(self, token): - """Mark the give token as revoked. - - :param dict token: token dict - """ - result = self.server.idps.getIdProviderFromToken(token["access_token"]) - if not result["OK"]: - raise OAuth2Error(result["Message"]) - if result["OK"]: - for tokenType in token: - result = result["Value"].revokeToken(token[tokenType], tokenType) - if not result["OK"]: - self.server.log.error(result["Message"]) - if not result["OK"]: - raise OAuth2Error(result["Message"]) diff --git a/src/DIRAC/FrameworkSystem/private/authorization/grants/__init__.py b/src/DIRAC/FrameworkSystem/private/authorization/grants/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/DIRAC/FrameworkSystem/private/authorization/utils/Clients.py b/src/DIRAC/FrameworkSystem/private/authorization/utils/Clients.py index e41e08c9b70..747612f49da 100644 --- a/src/DIRAC/FrameworkSystem/private/authorization/utils/Clients.py +++ b/src/DIRAC/FrameworkSystem/private/authorization/utils/Clients.py @@ -1,9 +1,3 @@ -import time - -from authlib.oauth2.rfc6749.util import scope_to_list, list_to_scope -from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin - -from DIRAC import gLogger from DIRAC.Core.Security import Locations from DIRAC.ConfigurationSystem.Client.Utilities import getAuthorizationServerMetadata @@ -46,37 +40,3 @@ def getDIRACClients(): else: clients[cli].update(confClients[cli]) return clients - - -class Client(OAuth2ClientMixin): - """This class describes the OAuth2 client.""" - - def __init__(self, params): - """C'r - - :param dict params: client parameters - """ - if params.get("redirect_uri") and not params.get("redirect_uris"): - params["redirect_uris"] = [params["redirect_uri"]] - self.set_client_metadata(params) - self.client_id = params["client_id"] - self.client_secret = params.get("client_secret", "") - self.client_id_issued_at = params.get("client_id_issued_at", int(time.time())) - self.client_secret_expires_at = params.get("client_secret_expires_at", 0) - - def get_allowed_scope(self, scope): - """Get allowed scope. Has been slightly modified to accommodate parametric scopes. - - :param str scope: requested scope - - :return: str -- scopes - """ - if not isinstance(scope, str): - scope = list_to_scope(scope) - allowed = scope_to_list(super().get_allowed_scope(scope)) - for s in scope_to_list(scope): - for def_scope in scope_to_list(self.scope): - if s.startswith(def_scope) and s not in allowed: - allowed.append(s) - gLogger.debug(f'Try to allow "{scope}" scope:', allowed) - return list_to_scope(list(set(allowed))) diff --git a/tests/Integration/Framework/Test_AuthDB.py b/tests/Integration/Framework/Test_AuthDB.py deleted file mode 100644 index db0ab0e6b7b..00000000000 --- a/tests/Integration/Framework/Test_AuthDB.py +++ /dev/null @@ -1,220 +0,0 @@ -""" This is a test of the AuthDB. Requires authlib - It supposes that the DB is present and installed in DIRAC -""" -import time - -import DIRAC - -DIRAC.initialize(require_auth=False, host_credentials=True) # Initialize configuration - -from authlib.jose import JsonWebKey, JsonWebSignature, jwt, RSAKey -from authlib.common.encoding import json_b64encode, urlsafe_b64decode, json_loads -from DIRAC.FrameworkSystem.DB.AuthDB import AuthDB - -db = AuthDB() - -payload = { - "sub": "user", - "iss": "issuer", - "iat": int(time.time()), - "exp": int(time.time()) + (12 * 3600), - "scope": "scope", - "setup": "setup", - "group": "my_group", -} - - -def test_RefreshToken(): - """Try to revoke/save/get refresh tokens""" - DToken = dict( - access_token=jwt.encode({"alg": "HS256"}, payload, "secret").decode("utf-8"), - refresh_token=jwt.encode({"alg": "HS256"}, payload, "secret").decode("utf-8"), - expires_at=int(time.time()) + 3600, - ) - - New_DToken = dict( - access_token=jwt.encode({"alg": "HS256"}, payload, "secret").decode("utf-8"), - refresh_token=jwt.encode({"alg": "HS256"}, payload, "secret").decode("utf-8"), - expires_in=int(time.time()) + 3600, - ) - - preset_jti = "123" - - # Remove refresh token - result = db.revokeRefreshToken(preset_jti) - assert result["OK"], result["Message"] - - # Store tokens - result = db.storeRefreshToken(DToken.copy(), preset_jti) - assert result["OK"], result["Message"] - assert result["Value"]["jti"] == preset_jti - assert result["Value"]["iat"] <= int(time.time()) - - result = db.storeRefreshToken(New_DToken.copy()) - assert result["OK"], result["Message"] - assert result["Value"]["jti"] - assert result["Value"]["iat"] <= int(time.time()) - - token_id = result["Value"]["jti"] - issued_at = result["Value"]["iat"] - - # Check token - result = db.getCredentialByRefreshToken(preset_jti) - assert result["OK"], result["Message"] - assert result["Value"]["jti"] == preset_jti - assert result["Value"]["access_token"] == DToken["access_token"] - assert result["Value"]["refresh_token"] == DToken["refresh_token"] - - result = db.getCredentialByRefreshToken(token_id) - assert result["OK"], result["Message"] - assert result["Value"]["jti"] == token_id - assert int(result["Value"]["issued_at"]) == issued_at - assert result["Value"]["access_token"] == New_DToken["access_token"] - assert result["Value"]["refresh_token"] == New_DToken["refresh_token"] - - # Check token after request - for jti in [token_id, preset_jti]: - result = db.getCredentialByRefreshToken(jti) - assert result["OK"], result["Message"] - assert not result["Value"] - - # Renew tokens - result = db.storeRefreshToken(New_DToken.copy(), token_id) - assert result["OK"], result["Message"] - - # Revoke token - result = db.revokeRefreshToken(token_id) - assert result["OK"], result["Message"] - - # Check token - result = db.getCredentialByRefreshToken(token_id) - assert result["OK"], result["Message"] - assert not result["Value"] - - -def test_keys(): - """Try to store/get/remove keys""" - # JWS - jws = JsonWebSignature(algorithms=["RS256"]) - code_payload = { - "user_id": "user", - "scope": "scope", - "client_id": "client", - "redirect_uri": "redirect_uri", - "code_challenge": "code_challenge", - } - - # Token metadata - header = {"alg": "RS256"} - payload = {"sub": "user", "iss": "issuer", "scope": "scope", "setup": "setup", "group": "my_group"} - - # Remove all keys - result = db.removeKeys() - assert result["OK"], result["Message"] - - # Check active keys - result = db.getActiveKeys() - assert result["OK"], result["Message"] - assert result["Value"] == [] - - # Create new one - result = db.getPrivateKey() - assert result["OK"], result["Message"] - - private_key = result["Value"] - assert isinstance(private_key, RSAKey) - - # Sign token - header["kid"] = private_key.thumbprint() - - # Find key by KID - result = db.getPrivateKey(header["kid"]) - assert result["OK"], result["Message"] - # as_dict has no arguments for authlib < 1.0.0 - # for authlib >= 1.0.0: - assert result["Value"].as_dict(True) == private_key.as_dict(True) - - # Sign token - token = jwt.encode(header, payload, private_key) - # Sign auth code - code = jws.serialize_compact(header, json_b64encode(code_payload), private_key) - - # Get public key set - result = db.getKeySet() - keyset = result["Value"] - assert result["OK"], result["Message"] - # as_dict has no arguments for authlib < 1.0.0 - # for authlib >= 1.0.0: - assert bool([key for key in keyset.as_dict(True)["keys"] if key["kid"] == header["kid"]]) - - # Read token - _payload = jwt.decode(token, JsonWebKey.import_key_set(keyset.as_dict())) - assert _payload == payload - # Read auth code - data = jws.deserialize_compact(code, keyset.keys[0]) - _code_payload = json_loads(urlsafe_b64decode(data["payload"])) - assert _code_payload == code_payload - - -def test_Sessions(): - """Try to store/get/remove Sessions""" - # Example of the new session metadata - sData1 = { - "client_id": "DIRAC_CLI", - "device_code": "SsoGTDglu6LThpx0CigM9i9J72B5atZ24ULr6R1awm", - "expires_in": 1800, - "id": "SsoGTDglu6LThpx0CigM9i9J72B5atZ24ULr6R1awm", - "interval": 5, - "scope": "g:my_group", - "uri": "https://domain.com/auth/device?&response_type=device&client_id=DIRAC_CLI&scope=g:my_group", - "user_code": "MDKP-MXMF", - "verification_uri": "https://domain.com/auth/device", - "verification_uri_complete": "https://domain.com/auth/device?user_code=MDKP-MXMF", - } - - # Example of the updated session - sData2 = { - "client_id": "DIRAC_CLI", - "device_code": "SsoGTDglu6LThpx0CigM9i9J72B5atZ24ULr6R1awm", - "expires_in": 1800, - "id": "SsoGTDglu6LThpx0CigM9i9J72B5atZ24ULr6R1awm", - "interval": 5, - "scope": "g:my_group", - "uri": "https://domain.com/auth/device?&response_type=device&client_id=DIRAC_CLI&scope=g:my_group", - "user_code": "MDKP-MXMF", - "verification_uri": "https://domain.com/auth/device", - "verification_uri_complete": "https://domain.com/auth/device?user_code=MDKP-MXMF", - "user_id": "username", - } - - # Remove old session - db.removeSession(sData1["id"]) - - # Add session - result = db.addSession(sData1) - assert result["OK"], result["Message"] - - # Get session - result = db.getSessionByUserCode(sData1["user_code"]) - assert result["OK"], result["Message"] - assert result["Value"]["device_code"] == sData1["device_code"] - assert result["Value"].get("user_id") != sData2["user_id"] - - # Update session - result = db.updateSession(sData2, sData1["id"]) - assert result["OK"], result["Message"] - - # Get session - result = db.getSession(sData2["id"]) - assert result["OK"], result["Message"] - assert result["Value"]["device_code"] == sData2["device_code"] - assert result["Value"]["user_id"] == sData2["user_id"] - - # Remove session - result = db.removeSession(sData2["id"]) - assert result["OK"], result["Message"] - - # Make sure that the session is absent - result = db.getSession(sData2["id"]) - assert result["OK"], result["Message"] - assert not result["Value"] diff --git a/tests/Integration/Framework/Test_AuthServer.py b/tests/Integration/Framework/Test_AuthServer.py deleted file mode 100644 index dc2e50ddefe..00000000000 --- a/tests/Integration/Framework/Test_AuthServer.py +++ /dev/null @@ -1,91 +0,0 @@ -""" This is a test of the AuthServer. Requires authlib, pyjwt, dominate - It supposes that the AuthDB is present and installed in DIRAC -""" -import pytest -from unittest.mock import MagicMock - -import DIRAC - -DIRAC.initialize(require_auth=False, host_credentials=True) # Initialize configuration - -from DIRAC import S_OK -from DIRAC.FrameworkSystem.private.authorization import AuthServer -from DIRAC.FrameworkSystem.private.authorization.utils import Utilities - - -class Proxy: - def dumpAllToString(self): - return S_OK("proxy") - - -class ProxyManagerClient: - def downloadProxy(self, *args, **kwargs): - return S_OK(Proxy()) - - -class TokenManagerClient: - def getToken(self, *args, **kwargs): - return S_OK({"access_token": "token", "refresh_token": "token"}) - - -mockgetIdPForGroup = MagicMock(return_value=S_OK("IdP")) -mockgetDNForUsername = MagicMock(return_value=S_OK("DN")) -mockgetUsernameForDN = MagicMock(return_value=S_OK("user")) -mockisDownloadProxyAllowed = MagicMock(return_value=True) -mockgetAuthorizationServerMetadata = MagicMock(return_value=S_OK(dict(issuer="https://issuer.url/"))) - - -@pytest.fixture -def auth_server(monkeypatch): - monkeypatch.setattr(Utilities, "getAuthorizationServerMetadata", mockgetAuthorizationServerMetadata) - monkeypatch.setattr(AuthServer, "collectMetadata", Utilities.collectMetadata) - monkeypatch.setattr(AuthServer, "getIdPForGroup", mockgetIdPForGroup) - monkeypatch.setattr(AuthServer, "getDNForUsername", mockgetDNForUsername) - monkeypatch.setattr(AuthServer, "getUsernameForDN", mockgetUsernameForDN) - monkeypatch.setattr(AuthServer, "ProxyManagerClient", ProxyManagerClient) - monkeypatch.setattr(AuthServer, "TokenManagerClient", TokenManagerClient) - monkeypatch.setattr(AuthServer, "isDownloadProxyAllowed", mockisDownloadProxyAllowed) - return AuthServer.AuthServer() - - -def test_metadata(auth_server): - """Check metadata""" - assert auth_server.metadata.get("issuer") - - -def test_queryClient(auth_server): - """Try to search some default client""" - assert not auth_server.query_client("not_exist_client") - assert auth_server.query_client("DIRAC_CLI").client_id == "DIRAC_CLI" - - -@pytest.mark.parametrize( - "client, grant, user, scope, expires_in, refresh_token, instance, result", - [ - ("DIRAC_CLI", None, "id", "g:my_group proxy", None, None, "proxy", "proxy"), - ("DIRAC_CLI", None, "id", "g:my_group", None, None, "access_token", "token"), - ], -) -def test_generateToken(auth_server, client, grant, user, scope, expires_in, refresh_token, instance, result): - """Generate tokens""" - from authlib.oauth2.base import OAuth2Error - - cli = auth_server.query_client(client) - try: - assert auth_server.generate_token(cli, grant, user, scope, expires_in, refresh_token).get(instance) == result - except OAuth2Error as e: - assert False, str(e) - - -def test_writeReadRefreshToken(auth_server): - """Try to search some default client""" - result = auth_server.registerRefreshToken({}, {"access_token": "token", "refresh_token": "token"}) - assert result["OK"], result["Message"] - token = result["Value"] - assert token.get("access_token") == "token" - assert token.get("refresh_token") != "token" - - result = auth_server.readToken(token["refresh_token"]) - assert result["OK"], result["Message"] - assert result["Value"].get("jti") - assert result["Value"].get("iat") diff --git a/tests/Integration/Framework/Test_TokenDB.py b/tests/Integration/Framework/Test_TokenDB.py index 557cdd6df80..2bb0c469679 100644 --- a/tests/Integration/Framework/Test_TokenDB.py +++ b/tests/Integration/Framework/Test_TokenDB.py @@ -1,4 +1,4 @@ -""" This is a test of the AuthDB. Requires authlib, pyjwt +""" This is a test of the TokenDB. Requires authlib, pyjwt It supposes that the DB is present and installed in DIRAC """ import time diff --git a/tests/Integration/all_integration_server_tests.sh b/tests/Integration/all_integration_server_tests.sh index f90d55594f6..f65f14bca4c 100644 --- a/tests/Integration/all_integration_server_tests.sh +++ b/tests/Integration/all_integration_server_tests.sh @@ -28,8 +28,6 @@ echo -e "*** $(date -u) **** FRAMEWORK TESTS ****\n" pytest --no-check-dirac-environment "${THIS_DIR}/Framework/Test_InstalledComponentsDB.py" |& tee -a "${SERVER_TEST_OUTPUT}"; (( ERR |= "${?}" )) pytest --no-check-dirac-environment "${THIS_DIR}/Framework/Test_ProxyDB.py" |& tee -a "${SERVER_TEST_OUTPUT}"; (( ERR |= "${?}" )) pytest --no-check-dirac-environment "${THIS_DIR}/Framework/Test_TokenDB.py" |& tee -a "${SERVER_TEST_OUTPUT}"; (( ERR |= "${?}" )) -pytest --no-check-dirac-environment "${THIS_DIR}/Framework/Test_AuthDB.py" |& tee -a "${SERVER_TEST_OUTPUT}"; (( ERR |= "${?}" )) -pytest --no-check-dirac-environment "${THIS_DIR}/Framework/Test_AuthServer.py" |& tee -a "${SERVER_TEST_OUTPUT}"; (( ERR |= "${?}" )) #-------------------------------------------------------------------------------# echo -e "*** $(date -u) **** MONITORING TESTS ****\n"