コード例 #1
0
ファイル: AuthServer.py プロジェクト: TaykYoku/DIRAC
 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
     # 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
コード例 #2
0
ファイル: OAuthDB.py プロジェクト: DIRACGrid/OAuthDIRAC
    def getAuthorization(self, providerName, session=None):
        """ Register new session and return dict with authorization url and session id
    
        :param basestring providerName: provider name
        :param basestring session: here is able to set session id(optional)

        :return: S_OK(dict)/S_ERROR() -- dictionary contain Status, Session, etc.
    """
        self.log.info('Get authorization for %s.' % providerName,
                      'Session: %s' % session if session else '')
        result = IdProviderFactory().getIdProvider(providerName)
        if not result['OK']:
            return result
        __provObj = result['Value']
        result = __provObj.checkStatus(session=session)
        if not result['OK']:
            return result
        statusDict = result['Value']

        # Create new session
        if statusDict['Status'] == 'needToAuth':
            result = self.insertFields(
                'Sessions', ['Session', 'Provider', 'Comment', 'LastAccess'], [
                    statusDict['Session'], providerName, statusDict['URL'],
                    'UTC_TIMESTAMP()'
                ])
            if not result['OK']:
                return result
            self.log.info(statusDict['Session'],
                          'session for %s created' % providerName)
            statusDict['URL'] = '%s/auth/%s' % (getAuthAPI().strip('/'),
                                                statusDict['Session'])
        return S_OK(statusDict)
コード例 #3
0
ファイル: Tokens.py プロジェクト: TaykYoku/DIRAC
    def getInfoAsString(self):
        """Return information about token as string

        :return: str
        """
        result = IdProviderFactory().getIdProviderForToken(
            self.get("access_token"))
        if not result["OK"]:
            return "Cannot load provider: %s" % result["Message"]
        cli = result["Value"]
        cli.token = self.copy()
        result = cli.verifyToken()
        if not result["OK"]:
            return result["Message"]
        payload = result["Value"]
        result = cli.researchGroup(payload)
        if not result["OK"]:
            return result["Message"]
        credDict = result["Value"]
        result = Registry.getUsernameForDN(credDict["DN"])
        if not result["OK"]:
            return result["Message"]
        credDict["username"] = result["Value"]
        if credDict.get("group"):
            credDict["properties"] = Registry.getPropertiesForGroup(
                credDict["group"])
        payload.update(credDict)
        return self.__formatTokenInfoAsString(payload)
コード例 #4
0
ファイル: OAuthDB.py プロジェクト: DIRACGrid/OAuthDIRAC
    def logOutSession(self, session):
        """ Remove session
    
        :param basestring session: session id

        :return: S_OK()/S_ERROR()
    """
        # Log out from provider
        result = self.__getFields(['Provider'], session=session)
        if not result['OK']:
            return result
        provider = result['Value']
        result = IdProviderFactory().getIdProvider(provider)
        if result['OK']:
            providerObj = result['Value']
            result = self.getTokensBySession(session)
            if not result['OK']:
                return result
            result = providerObj.logOut(result['Value'])
        self.log.debug('%s log out:',
                       result.get('Message') or result.get('Value'))
        return self.killSession(session)
コード例 #5
0
ファイル: OAuthDB.py プロジェクト: DIRACGrid/OAuthDIRAC
    def fetchReservedSessions(self):
        """ Fetch reserved sessions

        :return: S_OK(int)/S_ERROR()
    """
        result = self.__getFields(Status="reserved")
        if not result['OK']:
            return result
        sessionsData = result['Value']
        self.log.info('Found %s reserved sessions to fetch' %
                      len(sessionsData))
        for i in range(0, len(sessions)):
            result = IdProviderFactory().getIdProvider(sessions[i]['Provider'])
            if result['OK']:
                providerObj = result['Value']
                result = providerObj.fetch(sessions[i])
コード例 #6
0
    def initializeHandler(cls, *args):
        """Initialization

        :return: S_OK()/S_ERROR()
        """
        # Let's try to connect to the database
        try:
            cls.__tokenDB = TokenDB(parentLogger=cls.log)
        except Exception as e:
            cls.log.exception(e)
            return S_ERROR(f"Could not connect to the database {repr(e)}")

        # Cache containing tokens from scope requested by the client
        cls.__tokensCache = DictCache()

        # The service plays an important OAuth 2.0 role, namely it is an Identity Provider client.
        # This allows you to manage tokens without the involvement of their owners.
        cls.idps = IdProviderFactory()
        return S_OK()
コード例 #7
0
    def checkStatus(self, userDN):
        """ Read ready to work status of proxy provider

        :param dict userDict: user description dictionary with possible fields:
               FullName, UserName, DN, Email, DiracGroup
        :param dict sessionDict: session dictionary

        :return: S_OK(dict)/S_ERROR() -- dictionary contain fields:
                 - 'Status' with ready to work status[ready, needToAuth]
                 - 'AccessTokens' with list of access token
    """
        result = Registry.getUserNameForDN(userDN)
        if not result['OK']:
            return result
        self.userName = result['Value']
        result = IdProviderFactory().getIdProvider(
            self.parameters['IdProvider'])
        if not result['OK']:
            return result
        self.idProvider = result['Value']
        self.oauth2 = OAuth2(self.parameters['IdProvider'])
        return self.idProvider.checkStatus(self.userName)
コード例 #8
0
def login(params):
    from prompt_toolkit import prompt, print_formatted_text, HTML
    from DIRAC.Resources.IdProvider.IdProviderFactory import IdProviderFactory
    from DIRAC.FrameworkSystem.private.authorization.utils.Tokens import (
        writeTokenDictToTokenFile,
        getTokenFileLocation,
        readTokenFromFile,
    )

    # Init authorization client
    result = IdProviderFactory().getIdProvider("DIRACCLI",
                                               issuer=params.issuer,
                                               scope=" ")
    if not result["OK"]:
        return result
    idpObj = result["Value"]

    # Get token file path
    tokenFile = getTokenFileLocation()

    # Submit Device authorisation flow
    if not (result := idpObj.deviceAuthorization())["OK"]:
        return result
コード例 #9
0
ファイル: TornadoBaseClient.py プロジェクト: TaykYoku/DIRAC
    def __discoverCredentialsToUse(self):
        """Discovers which credentials to use for connection.
        * Server certificate:
          -> If KW_USE_CERTIFICATES in kwargs, sets it in self.__useCertificates
          -> If not, check gConfig.useServerCertificate(), and sets it in self.__useCertificates
              and kwargs[KW_USE_CERTIFICATES]
        * Certification Authorities check:
           -> if KW_SKIP_CA_CHECK is not in kwargs and we are using the certificates,
                set KW_SKIP_CA_CHECK to false in kwargs
           -> if KW_SKIP_CA_CHECK is not in kwargs and we are not using the certificate, check the skipCACheck
        * Bearer token:
          -> If KW_USE_ACCESS_TOKEN in kwargs, sets it in self.__useAccessToken
          -> If not, check "/DIRAC/Security/UseTokens", and sets it in self.__useAccessToken
              and kwargs[KW_USE_ACCESS_TOKEN]
          -> If not, check 'DIRAC_USE_ACCESS_TOKEN' in os.environ, sets it in self.__useAccessToken
              and kwargs[KW_USE_ACCESS_TOKEN]
        * Proxy Chain

        WARNING: MOSTLY COPY/PASTE FROM Core/Diset/private/BaseClient

        """
        # Use certificates?
        if self.KW_USE_CERTIFICATES in self.kwargs:
            self.__useCertificates = self.kwargs[self.KW_USE_CERTIFICATES]
        else:
            self.__useCertificates = gConfig.useServerCertificate()
            self.kwargs[self.KW_USE_CERTIFICATES] = self.__useCertificates
        if self.KW_SKIP_CA_CHECK not in self.kwargs:
            if self.__useCertificates:
                self.kwargs[self.KW_SKIP_CA_CHECK] = False
            else:
                self.kwargs[self.KW_SKIP_CA_CHECK] = skipCACheck()

        # Use tokens?
        if self.KW_USE_ACCESS_TOKEN in self.kwargs:
            self.__useAccessToken = self.kwargs[self.KW_USE_ACCESS_TOKEN]
        elif "DIRAC_USE_ACCESS_TOKEN" in os.environ:
            self.__useAccessToken = os.environ.get("DIRAC_USE_ACCESS_TOKEN",
                                                   "false").lower() in ("y",
                                                                        "yes",
                                                                        "true")
        else:
            self.__useAccessToken = gConfig.getValue(
                "/DIRAC/Security/UseTokens", "false").lower() in (
                    "y",
                    "yes",
                    "true",
                )
        self.kwargs[self.KW_USE_ACCESS_TOKEN] = self.__useAccessToken

        if self.__useAccessToken:
            from DIRAC.Resources.IdProvider.IdProviderFactory import IdProviderFactory

            result = IdProviderFactory().getIdProvider("DIRACCLI")
            if not result["OK"]:
                return result
            self.__idp = result["Value"]

        # Rewrite a little bit from here: don't need the proxy string, we use the file
        if self.KW_PROXY_CHAIN in self.kwargs:
            try:
                self.kwargs[self.KW_PROXY_STRING] = self.kwargs[
                    self.KW_PROXY_CHAIN].dumpAllToString()["Value"]
                del self.kwargs[self.KW_PROXY_CHAIN]
            except Exception:
                return S_ERROR(
                    "Invalid proxy chain specified on instantiation")

        # ==== REWRITED FROM HERE ====

        # For certs always check CA's. For clients skipServerIdentityCheck

        return S_OK()
コード例 #10
0
      client_id = IdP_client_id
      client_secret = IdP_client_secret
      redirect_uri = https://dirac/redirect
      jwks_uri = https://idp.url/jwk
      scope = openid+profile+offline_access+eduperson_entitlement
    }
  }
}
""")
gConfig.loadCFG(cfg)

from authlib.jose import jwt
from DIRAC.Resources.IdProvider.IdProviderFactory import IdProviderFactory
from DIRAC.FrameworkSystem.private.authorization.utils.Clients import DEFAULT_CLIENTS

idps = IdProviderFactory()


def test_getDIRACClients():
    """Try to load default DIRAC authorization client"""
    # Try to get DIRAC client authorization settings
    result = idps.getIdProvider("DIRACCLI")
    assert result["OK"], result["Message"]
    assert result["Value"].issuer == "https://issuer.url/"
    assert result["Value"].client_id == DEFAULT_CLIENTS["DIRACCLI"][
        "client_id"]
    assert result["Value"].get_metadata("jwks_uri") == "https://issuer.url/jwk"

    # Try to get DIRAC client authorization settings for Web portal
    result = idps.getIdProvider("DIRACWeb")
    assert result["OK"], result["Message"]
コード例 #11
0
    def doOAuthMagic(self):
        """Magic method with tokens

        :return: S_OK()/S_ERROR()
        """
        params = {}
        if self.issuer:
            params["issuer"] = self.issuer
        result = IdProviderFactory().getIdProvider("DIRACCLI", **params)
        if not result["OK"]:
            return result
        idpObj = result["Value"]
        if self.group and self.group not in self.scopes:
            self.scopes.append(f"g:{self.group}")
        if self.response == "proxy" and self.response not in self.scopes:
            self.scopes.append(self.response)
        if self.lifetime:
            self.scopes.append("lifetime:%s" % (int(self.lifetime or 12) * 3600))
        idpObj.scope = "+".join(self.scopes) if self.scopes else ""

        # Submit Device authorisation flow
        result = idpObj.deviceAuthorization()
        if not result["OK"]:
            return result

        if self.response == "proxy":
            self.outputFile = self.outputFile or getDefaultProxyLocation()
            # Save new proxy certificate
            result = writeToProxyFile(idpObj.token["proxy"].encode("UTF-8"), self.outputFile)
            if not result["OK"]:
                return result
            gLogger.notice(f"Proxy is saved to {self.outputFile}.")
        else:
            # Revoke old tokens from token file
            self.outputFile = getTokenFileLocation(self.outputFile)
            if os.path.isfile(self.outputFile):
                result = readTokenFromFile(self.outputFile)
                if not result["OK"]:
                    gLogger.error(result["Message"])
                elif result["Value"]:
                    oldToken = result["Value"]
                    for tokenType in ["access_token", "refresh_token"]:
                        result = idpObj.revokeToken(oldToken[tokenType], tokenType)
                        if result["OK"]:
                            gLogger.notice(f"{tokenType} is revoked from", self.outputFile)
                        else:
                            gLogger.error(result["Message"])

            # Save new tokens to token file
            result = writeTokenDictToTokenFile(idpObj.token, self.outputFile)
            if not result["OK"]:
                return result
            self.outputFile = result["Value"]
            gLogger.notice(f"New token is saved to {self.outputFile}.")

            if not DIRAC.gConfig.getValue("/DIRAC/Security/Authorization/issuer"):
                gLogger.notice("To continue use token you need to add /DIRAC/Security/Authorization/issuer option.")
                if not self.issuer:
                    DIRAC.exit(1)
                DIRAC.gConfig.setOptionValue("/DIRAC/Security/Authorization/issuer", self.issuer)

        return S_OK()
コード例 #12
0
ファイル: OAuthDB.py プロジェクト: DIRACGrid/OAuthDIRAC
    def updateIdPSessionsInfoCache(self, idPs=None, IDs=None):
        """ Update cache with information about active session with identity provider

        :param list idPs: list of identity providers that sessions need to update, if None - update all
        :param list IDs: list of IDs that need to update, if None - update all

        :return: S_OK()/S_ERROR()
    """
        IdPSessionsInfo = {}
        result = self._query(
            "SELECT DISTINCT ID, Provider, Session FROM `Sessions`")
        if not result['OK']:
            return result
        for ID, idP, session in result['Value']:
            if (idPs and idP not in idPs) or (IDs and ID not in IDs):
                continue
            if ID not in IdPSessionsInfo:
                IdPSessionsInfo[ID] = {'Providers': []}
            if idP not in IdPSessionsInfo[ID]:
                result = IdProviderFactory().getIdProvider(idP)
                if not result['OK']:
                    return result
                __provObj = result['Value']
                result = __provObj.getUserProfile(session)
                if not result['OK']:
                    self.log.error(result['Message'])
                    kill = self.killSession(session)
                    self.log.warn(
                        'Cannot get user profile for %s session, removed.' %
                        session,
                        kill.get('Value') or kill.get('Message'))
                    continue
                userProfile = result['Value']
                result = self.getTokensBySession(session)
                if not result['OK']:
                    return result
                tokens = result['Value']
                if not tokens:
                    result = self.killSession(session)
                    self.log.warn(
                        'Not found tokens for %s session, removed.' % session,
                        result.get('Value') or result.get('Message'))
                    continue
                IdPSessionsInfo[ID][idP] = {session: tokens}
                IdPSessionsInfo[ID]['Providers'] = list(
                    set(IdPSessionsInfo[ID]['Providers'] + [idP]))
                # Fill user profile
                for key, value in userProfile.items():
                    if key in IdPSessionsInfo[ID]:
                        continue
                    IdPSessionsInfo[ID][key] = value
            else:
                result = self.getTokensBySession(session)
                if not result['OK']:
                    return result
                tokens = result['Value']
                if not tokens:
                    result = self.killSession(session)
                    self.log.warn(
                        'Not found tokens for %s session, removed.' % session,
                        result.get('Value') or result.get('Message'))
                    continue
                IdPSessionsInfo[ID][idP][session] = tokens

        return S_OK(IdPSessionsInfo)
コード例 #13
0
ファイル: OAuthDB.py プロジェクト: DIRACGrid/OAuthDIRAC
    def __parse(self, response, session):
        """ Parsing response

        :param dict response: authorization response dictionary
        :param basestring session: session id

        :return: S_OK(dict)/S_ERROR
    """
        # Search provider by session
        result = self.__getFields(['Provider'], session=session)
        if not result['OK']:
            return result
        providerName = result['Value']['Provider']
        result = IdProviderFactory().getIdProvider(providerName)
        if not result['OK']:
            return result
        __provObj = result['Value']

        # Parsing response
        self.log.info(
            session,
            'session, parsing "%s" authentication response.' % providerName)
        result = __provObj.parseAuthResponse(response)
        if not result['OK']:
            return result
        parseDict = result['Value']

        status = 'authed'
        comment = ''
        __mail = {}
        result = getUsernameForID(parseDict['UsrOptns']['ID'])
        if not result['OK']:
            groups = []
            for dn in parseDict['UsrOptns']['DNs'].keys():
                result = getGroupsForDN(dn)
                if not result['OK']:
                    return result
                groups = list(set(groups + result['Value']))
            if groups:
                status = 'authed and notify'
                comment = 'Administrators was notified about you. Found new groups %s' % groups
                __mail[
                    'subject'] = "[OAuthManager] User %s to be added." % parseDict[
                        'username']
                __mail['body'] = 'User %s was authenticated by ' % parseDict[
                    'UsrOptns']['FullName']
                __mail['body'] += providerName
                __mail[
                    'body'] += "\n\nAuto updating of the user database is not allowed."
                __mail['body'] += " New user %s to be added," % parseDict[
                    'username']
                __mail['body'] += "with the following information:\n"
                __mail['body'] += "\nUser name: %s\n" % parseDict['username']
                __mail['body'] += "\nUser profile:\n%s" % pprint.pformat(
                    parseDict['UsrOptns'])
                __mail['body'] += "\n\n------"
                __mail[
                    'body'] += "\n This is a notification from the DIRAC OAuthManager service, please do not reply.\n"
            else:
                status = 'visitor'
                comment = 'We not found any registred DIRAC groups that mached with your profile. '
                comment += 'So, your profile has the same access that Visitor DIRAC user.'
                comment += 'Your ID: 100492713487956493716'
                result = self.updateSession(
                    {
                        'ID': parseDict['UsrOptns']['ID'],
                        'Status': status,
                        'Comment': comment
                    },
                    session=session)
                if not result['OK']:
                    return result
                return S_OK((parseDict, status, comment, __mail))

        if not parseDict['Tokens'].get('RefreshToken'):
            return S_ERROR('No refresh token found in response.')

        # If current session is session to reserve
        if re.match('^reserved_.*', session):
            # Update status in source session
            result = self.updateSession(
                {
                    'ID': parseDict['UsrOptns']['ID'],
                    'Status': status,
                    'Comment': comment
                },
                session=session.replace('reserved_', ''))
            if not result['OK']:
                return result
            # Update status in current session
            result = self.updateSession(
                {
                    'ID': parseDict['UsrOptns']['ID'],
                    'ExpiresIn': parseDict['Tokens']['ExpiresIn'],
                    'TokenType': parseDict['Tokens']['TokenType'],
                    'AccessToken': parseDict['Tokens']['AccessToken'],
                    'RefreshToken': parseDict['Tokens']['RefreshToken'],
                    'Status': 'reserved',
                    'Comment': comment
                },
                session=session)
            if not result['OK']:
                return result
            return S_OK((parseDict, status, comment, __mail))

        # If current session is not reserve, search reserved session
        result = self._query(
            'SELECT Session FROM `Sessions` WHERE ID="%s" AND Provider="%s"' %
            (parseDict['UsrOptns']['ID'], providerName))
        if not result['OK']:
            return result

        if not any(re.match('^reserved_.*', s[0]) for s in result['Value']):
            # If no found reserved session
            if status == 'authed':
                # If current session will use, need to redirect to create reserved session
                result = self.getAuthorization(providerName,
                                               session='reserved_%s' % session)
                if not result['OK']:
                    return result
                url = result['Value']['URL']
                # Save tokens to current session
                result = self.updateSession(
                    {
                        'ID': parseDict['UsrOptns']['ID'],
                        'ExpiresIn': parseDict['Tokens']['ExpiresIn'],
                        'TokenType': parseDict['Tokens']['TokenType'],
                        'AccessToken': parseDict['Tokens']['AccessToken'],
                        'RefreshToken': parseDict['Tokens']['RefreshToken'],
                        'Status': 'redirect',
                        'Comment': comment
                    },
                    session=session)
                if not result['OK']:
                    return result
                return S_OK((parseDict, 'redirect', url, __mail))

            # If notified, its mean that current session will not use and we can reserve it
            fillDict = {
                'ID': parseDict['UsrOptns']['ID'],
                'Status': 'reserved',
                'Comment': '',
                'Session': 'reserved_%s' % session,
                'Provider': providerName,
                'ExpiresIn': parseDict['Tokens']['ExpiresIn'],
                'TokenType': parseDict['Tokens']['TokenType'],
                'AccessToken': parseDict['Tokens']['AccessToken'],
                'RefreshToken': parseDict['Tokens']['RefreshToken'],
                'LastAccess': 'UTC_TIMESTAMP()'
            }
            result = self.insertFields('Sessions', fillDict.keys(),
                                       fillDict.values())
            if not result['OK']:
                return result
            self.log.info(session, 'session was reserved')
            result = self.updateSession(
                {
                    'ID': parseDict['UsrOptns']['ID'],
                    'Status': status,
                    'Comment': comment
                },
                session=session)
            if not result['OK']:
                return result
            return S_OK((parseDict, status, comment, __mail))

        result = self.updateSession(
            {
                'ID': parseDict['UsrOptns']['ID'],
                'ExpiresIn': parseDict['Tokens']['ExpiresIn'],
                'TokenType': parseDict['Tokens']['TokenType'],
                'AccessToken': parseDict['Tokens']['AccessToken'],
                'RefreshToken': parseDict['Tokens']['RefreshToken'],
                'Status': status,
                'Comment': comment
            },
            session=session)
        if not result['OK']:
            return result
        return S_OK((parseDict, status, comment, __mail))
コード例 #14
0
ファイル: AuthServer.py プロジェクト: TaykYoku/DIRAC
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
        # 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

    # 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("Try to query %s client" % client_id)
        clients = getDIRACClients()
        for cli in clients:
            if client_id == clients[cli]["client_id"]:
                gLogger.debug("Found %s client:\n" % cli, 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("%s:" % 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 identificator
        :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(
                "Try to query %s@%s proxy%s" % (user, group, ("with lifetime:%s" % 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("Try to get proxy for %s" % 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("Try to parse authentification response from %s:\n" % providerName, 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 registred?
        result = getUsernameForDN(credDict["DN"])
        if not result["OK"]:
            comment = f"ID {credDict['ID']} is not registred in DIRAC. "
            payload.update(idpObj.getUserProfile().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.tokenCli.updateToken(idpObj.token, credDict["ID"], idpObj.name)
        return S_OK(credDict) if result["OK"] else result

    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, HttpRequest, True)

    def validate_requested_scope(self, scope, state=None):
        """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(AuthServer, self).validate_requested_scope(extended_scope, state)

    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("<!DOCTYPE html>") 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")
            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 registred 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 requested access token for group that is not registred 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("The %s group belongs to the VO that is not tied to any Identity Provider." % request.group)

        sLog.debug("Check if %s identity provider registred in DIRAC.." % request.provider)
        # Research supported IdPs
        result = getProvidersForInstance("Id")
        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("%s identity provider is not registered." % request.provider)
            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 = getProviderInfo(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="%s/authorization/%s?%s" % (self.LOCATION, 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"] = "[DIRAC AS] User %s to be added." % username
        mail["body"] = "User %s was authenticated by %s." % (username, provider)
        mail["body"] += "\n\nNew user to be added with the following information:\n%s" % pprint.pformat(payload)
        mail["body"] += "\n\nPlease, add '%s' to /Register/Users/<username>/DN option.\n" % wrapIDAsDN(username)
        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
コード例 #15
0
 def __init__(self, **kwargs):
     super(TokenManagerClient, self).__init__(**kwargs)
     self.setServer("Framework/TokenManager")
     self.__tokensCache = DictCache()
     self.idps = IdProviderFactory()
コード例 #16
0
class TokenManagerClient(Client):
    """Client exposing the TokenManager Service."""

    DEFAULT_RT_EXPIRATION_TIME = 24 * 3600

    def __init__(self, **kwargs):
        super(TokenManagerClient, self).__init__(**kwargs)
        self.setServer("Framework/TokenManager")
        self.__tokensCache = DictCache()
        self.idps = IdProviderFactory()

    @gTokensSync
    def getToken(
        self,
        username: str,
        userGroup: str = None,
        scope: str = None,
        audience: str = None,
        identityProvider: str = None,
        requiredTimeLeft: int = 0,
    ):
        """Get an access token for a user/group.

        :param username: user name
        :param userGroup: group name
        :param scope: scope
        :param audience: audience
        :param identityProvider: identity Provider
        :param requiredTimeLeft: required time

        :return: S_OK(dict)/S_ERROR()
        """
        if not identityProvider and userGroup:
            identityProvider = Registry.getIdPForGroup(userGroup)
        if not identityProvider:
            return S_ERROR(
                f"The {userGroup} group belongs to a VO that is not tied to any Identity Provider."
            )

        # prepare the client instance of the appropriate IdP
        result = self.idps.getIdProvider(identityProvider)
        if not result["OK"]:
            return result
        idpObj = result["Value"]

        if userGroup and (result := idpObj.getGroupScopes(userGroup))["OK"]:
            # What scope correspond to the requested group?
            scope = list(set((scope or []) + result["Value"]))

        # Set the scope
        idpObj.scope = " ".join(scope)

        # Let's check if there are corresponding tokens in the cache
        cacheKey = (username, idpObj.scope, audience, identityProvider)
        if self.__tokensCache.exists(cacheKey, requiredTimeLeft):
            # Well we have a fresh record containing a Token object
            token = self.__tokensCache.get(cacheKey)
            # Let's check if the access token is fresh
            if not token.is_expired(requiredTimeLeft):
                return S_OK(token)
            # It seems that it is no longer valid for us, but whether there is a refresh token?
            if token.get("refresh_token"):
                # Okay, so we can try to refresh tokens
                if (result :=
                        idpObj.refreshToken(token["refresh_token"]))["OK"]:
                    # caching new tokens
                    self.__tokensCache.add(
                        cacheKey,
                        token.get_claim("exp", "refresh_token")
                        or self.DEFAULT_RT_EXPIRATION_TIME,
                        result["Value"],
                    )
                    return result
                self.log.verbose(
                    f"Failed to get token on client's side: {result['Message']}"
                )
                # Let's try to revoke broken token
                idpObj.revokeToken(token["refresh_token"])