コード例 #1
0
 def __init__(self):
     self.__parse_args()
     url = urlparse(self.__idcs_url)
     self.__host = url.hostname
     # logger used for HTTP request logging
     self.__logger = self.__get_logger()
     self.__logutils = LogUtils(self.__logger)
     self.__sess = Session()
     self.__request_utils = RequestUtils(self.__sess, self.__logutils)
コード例 #2
0
ファイル: idcs.py プロジェクト: y-polonsk/nosql-python-sdk
    def set_logger(self, logger):
        """
        Sets a logger instance for this provider. If not set, the logger
        associated with the driver is used.

        :param logger: the logger.
        :returns: self.
        :raises IllegalArgumentException: raises the exception if logger is not
            an instance of Logger.
        """
        CheckValue.check_logger(logger, 'logger')
        self.__logger = logger
        self.__logutils = LogUtils(logger)
        self.__request_utils = RequestUtils(self.__sess, self.__logutils)
        return self
コード例 #3
0
ファイル: idcs.py プロジェクト: y-polonsk/nosql-python-sdk
 def __init__(
         self,
         idcs_props_file=_DEFAULT_PROPS_FILE,
         idcs_url=None,
         entitlement_id=None,
         creds_provider=None,
         timeout_ms=Utils.DEFAULT_TIMEOUT_MS,
         cache_duration_seconds=AccessTokenProvider.MAX_ENTRY_LIFE_TIME,
         refresh_ahead=AccessTokenProvider.DEFAULT_REFRESH_AHEAD):
     # Constructs a default access token provider.
     if idcs_url is None:
         CheckValue.check_str(idcs_props_file, 'idcs_props_file')
         super(DefaultAccessTokenProvider, self).__init__(
             DefaultAccessTokenProvider.MAX_ENTRY_LIFE_TIME,
             DefaultAccessTokenProvider.DEFAULT_REFRESH_AHEAD)
         self.__idcs_url = self.__get_idcs_url(idcs_props_file)
         entitlement = self.__get_entitlement_id(idcs_props_file)
         self.__creds_provider = (
             PropertiesCredentialsProvider().set_properties_file(
                 self.__get_credential_file(idcs_props_file)))
         self.__timeout_ms = Utils.DEFAULT_TIMEOUT_MS
     else:
         CheckValue.check_str(idcs_url, 'idcs_url')
         self.__is_credentials_provider(creds_provider)
         CheckValue.check_int_gt_zero(timeout_ms, 'timeout_ms')
         CheckValue.check_int_gt_zero(cache_duration_seconds,
                                      'cache_duration_seconds')
         CheckValue.check_int_gt_zero(refresh_ahead, 'refresh_ahead')
         super(DefaultAccessTokenProvider,
               self).__init__(cache_duration_seconds, refresh_ahead)
         self.__idcs_url = idcs_url
         entitlement = entitlement_id
         self.__creds_provider = (PropertiesCredentialsProvider()
                                  if creds_provider is None else
                                  creds_provider)
         self.__timeout_ms = timeout_ms
     url = urlparse(self.__idcs_url)
     self.__host = url.hostname
     self.__andc_fqs = None
     if entitlement is not None:
         CheckValue.check_str(entitlement, 'entitlement_id')
         self.__andc_fqs = (AccessTokenProvider.ANDC_AUD_PREFIX +
                            entitlement + AccessTokenProvider.SCOPE)
     self.__psm_fqs = None
     self.__logger = None
     self.__logutils = LogUtils()
     self.__sess = Session()
     self.__request_utils = RequestUtils(self.__sess, self.__logutils)
コード例 #4
0
ファイル: idcs.py プロジェクト: y-polonsk/nosql-python-sdk
class DefaultAccessTokenProvider(AccessTokenProvider):
    """
    An instance of :py:class:`AccessTokenProvider` that acquires access tokens
    from Oracle Identity Cloud Service (IDCS) using information provided by
    :py:class:`CredentialsProvider`. By default the
    :py:class:`CredentialsProvider` is used.

    This class requires tenant-specific information in order to properly
    communicate with IDCS using the credential information:

        A tenant-specific URL used to communicate with IDCS.\n
        An entitlement id. This is generated by Oracle during account creation.

    The tenant-specific IDCS URL is the IDCS host assigned to the tenant. After
    logging into the IDCS admin console, copy the host of the IDCS admin console
    URL. For example, the format of the admin console URL is
    "https\://{tenantId}.identity.oraclecloud.com/ui/v1/adminconsole". The
    "https\://{tenantId}.identity.oraclecloud.com" portion is the required
    parameter.

    The entitlement id can be found using the IDCS admin console. After logging
    into the IDCS admin console, choose *Applications* from the button on the
    top left. Find the Application named ANDC, enter the Resources tab in the
    Configuration. There is a field called primary audience, the entitlement id
    parameter is the value of "urn\:opc\:andc\:entitlementid", which is treated
    as a string. For example if your primary audience is
    "urn\:opc\:andc\:entitlementid=123456789" then the parameter is "123456789"

    NOTE: above is simple python doc. This information is on the implementation.

    ATs are acquiring using OAuth client ID and secret pair along with user name
    and password using `Resource Owner Grant Type <https://docs.oracle.com/en/\
    cloud/paas/identity-cloud/rest-api/ROPCGT.html>`_

    This provider uses :py:class:`PropertiesCredentialsProvider` by default to
    obtain credentials. These credentials are used to build IDCS access token
    payloads to acquire the required access tokens. The expiry window is a
    constant 120,000 milliseconds.

    :param idcs_props_file: the path of an IDCS properties file.
    :param idcs_url: an IDCS URL, provided by IDCS.
    :param entitlement_id: service entitlement ID, which can be found from the
        primary audience of the application named ANDC from IDCS.
    :param creds_provider: a credentials provider.
    :param timeout_ms: the access token acquisition request timeout in
        milliseconds.
    :param cache_duration_seconds: the amount of time the access tokens are
        cached in the provider, in seconds.
    :param refresh_ahead: the refresh time before AT expired from cache.
    :raises IllegalArgumentException: raises the exception if parameters are not
        expected type.
    """
    # Payload used to acquire access token with resource owner grant flow.
    _RO_GRANT_FORMAT = 'grant_type=password&username={0}&scope={1}&password='******'grant_type=client_credentials&scope=urn:opc:idm:__myscopes__')

    # IDCS fully qualified scope to acquire IDCS AT.
    _IDCS_SCOPE = 'urn:opc:idm:__myscopes__'

    # Filter using service type URN to get PSMApp metadata from IDCS when
    # provider attempts to acquire PSM AT using PSMApp client id and secret. The
    # PSMApp metadata contains PSMApp client id, secret and primary audience of
    # PSM for this cloud account.
    _PSM_APP_FILTER = '?filter=serviceTypeURN+eq+%22PSMResourceTenatApp%22'

    # Filter using OAuth client id to find client metadata from IDCS.
    _CLIENT_FILTER = '?filter=name+eq+'

    # Field name in the IDCS access token response.
    _AT_FIELD = 'access_token'

    # Properties in the IDCS properties file.
    _IDCS_URL_PROP = 'idcs_url'
    _CREDS_FILE_PROP = 'creds_file'
    _ENTITLEMENT_ID_PROP = 'entitlement_id'

    # Default properties file at ~/.andc/idcs.props
    _DEFAULT_PROPS_FILE = environ['HOME'] + sep + '.andc' + sep + 'idcs.props'

    def __init__(
            self,
            idcs_props_file=_DEFAULT_PROPS_FILE,
            idcs_url=None,
            entitlement_id=None,
            creds_provider=None,
            timeout_ms=Utils.DEFAULT_TIMEOUT_MS,
            cache_duration_seconds=AccessTokenProvider.MAX_ENTRY_LIFE_TIME,
            refresh_ahead=AccessTokenProvider.DEFAULT_REFRESH_AHEAD):
        # Constructs a default access token provider.
        if idcs_url is None:
            CheckValue.check_str(idcs_props_file, 'idcs_props_file')
            super(DefaultAccessTokenProvider, self).__init__(
                DefaultAccessTokenProvider.MAX_ENTRY_LIFE_TIME,
                DefaultAccessTokenProvider.DEFAULT_REFRESH_AHEAD)
            self.__idcs_url = self.__get_idcs_url(idcs_props_file)
            entitlement = self.__get_entitlement_id(idcs_props_file)
            self.__creds_provider = (
                PropertiesCredentialsProvider().set_properties_file(
                    self.__get_credential_file(idcs_props_file)))
            self.__timeout_ms = Utils.DEFAULT_TIMEOUT_MS
        else:
            CheckValue.check_str(idcs_url, 'idcs_url')
            self.__is_credentials_provider(creds_provider)
            CheckValue.check_int_gt_zero(timeout_ms, 'timeout_ms')
            CheckValue.check_int_gt_zero(cache_duration_seconds,
                                         'cache_duration_seconds')
            CheckValue.check_int_gt_zero(refresh_ahead, 'refresh_ahead')
            super(DefaultAccessTokenProvider,
                  self).__init__(cache_duration_seconds, refresh_ahead)
            self.__idcs_url = idcs_url
            entitlement = entitlement_id
            self.__creds_provider = (PropertiesCredentialsProvider()
                                     if creds_provider is None else
                                     creds_provider)
            self.__timeout_ms = timeout_ms
        url = urlparse(self.__idcs_url)
        self.__host = url.hostname
        self.__andc_fqs = None
        if entitlement is not None:
            CheckValue.check_str(entitlement, 'entitlement_id')
            self.__andc_fqs = (AccessTokenProvider.ANDC_AUD_PREFIX +
                               entitlement + AccessTokenProvider.SCOPE)
        self.__psm_fqs = None
        self.__logger = None
        self.__logutils = LogUtils()
        self.__sess = Session()
        self.__request_utils = RequestUtils(self.__sess, self.__logutils)

    def set_credentials_provider(self, provider):
        """
        Sets :py:class:`CredentialsProvider`.

        :param provider: the credentials provider.
        :returns: self.
        :raises IllegalArgumentException: raises the exception if provider is
            not an instance of CredentialsProvider.
        """
        self.__is_credentials_provider(provider)
        self.__creds_provider = provider
        return self

    def set_logger(self, logger):
        """
        Sets a logger instance for this provider. If not set, the logger
        associated with the driver is used.

        :param logger: the logger.
        :returns: self.
        :raises IllegalArgumentException: raises the exception if logger is not
            an instance of Logger.
        """
        CheckValue.check_logger(logger, 'logger')
        self.__logger = logger
        self.__logutils = LogUtils(logger)
        self.__request_utils = RequestUtils(self.__sess, self.__logutils)
        return self

    def get_logger(self):
        """
        Returns the logger of this provider if set, None if not.

        :returns: the logger.
        """
        return self.__logger

    def get_account_access_token(self):
        self.__ensure_creds_provider()
        self.__find_oauth_scopes()
        if self.__psm_fqs is not None:
            return self.__get_at_by_password(self.__psm_fqs)
        return self.__get_at_by_psm_app()

    def get_service_access_token(self):
        self.__ensure_creds_provider()
        if self.__andc_fqs is None:
            self.__find_oauth_scopes()
        if self.__andc_fqs is None:
            raise IllegalStateException(
                'Unable to find service scope, OAuth client isn\'t ' +
                'configured properly, run OAuthClient utility to verify and ' +
                'recreate.')
        return self.__get_at_by_password(self.__andc_fqs)

    def close(self):
        super(DefaultAccessTokenProvider, self).close()
        if self.__sess is not None:
            self.__sess.close()

    def __ensure_creds_provider(self):
        if self.__creds_provider is None:
            raise IllegalArgumentException('CredentialsProvider unavailable.')

    def __find_oauth_scopes(self):
        # Find PSM and ANDC FQS from allowed scopes of OAuth client.
        if self.__andc_fqs is not None and self.__psm_fqs is not None:
            return
        creds = self.__get_client_creds()
        oauth_id = creds.get_credential_alias()
        auth = self.__get_auth_header(oauth_id, creds.get_secret())
        try:
            auth = 'Bearer ' + self.__get_access_token(
                auth, DefaultAccessTokenProvider._CLIENT_GRANT_PAYLOAD,
                DefaultAccessTokenProvider._IDCS_SCOPE)
        except InvalidAuthorizationException:
            self.__logutils.log_debug(
                'Cannot get access token with IDCS scope using client grant.')
            return
        response = self.__request_utils.do_get_request(
            self.__idcs_url + Utils.APP_ENDPOINT +
            DefaultAccessTokenProvider._CLIENT_FILTER + '%22' + oauth_id +
            '%22', Utils.scim_headers(self.__host, auth), self.__timeout_ms)
        if response is None:
            raise IllegalStateException(
                'Error getting client metadata from Identity Cloud Service, ' +
                'no response.')
        if response.get_status_code() >= codes.multiple_choices:
            Utils.handle_idcs_errors(
                response, 'Getting client metadata',
                'Please verify if the OAuth client is configured properly.')
        fqs_list = Utils.get_field(response.get_content(), 'allowedScopes',
                                   'fqs')
        if fqs_list is None:
            return
        for fqs in fqs_list:
            if fqs.startswith(AccessTokenProvider.ANDC_AUD_PREFIX):
                self.__andc_fqs = fqs
            elif fqs.endswith(Utils.PSM_SCOPE):
                self.__psm_fqs = fqs

    def __get_access_token(self, auth_header, payload, fqs):
        response = self.__request_utils.do_post_request(
            self.__idcs_url + Utils.TOKEN_ENDPOINT,
            Utils.token_headers(self.__host, auth_header), payload,
            self.__timeout_ms)
        if response is None:
            raise IllegalStateException('Error acquiring access token with '
                                        'scope ' + fqs + ', no response')
        response_code = response.get_status_code()
        content = response.get_content()
        if response_code >= codes.multiple_choices:
            self.__handle_token_error_response(response_code, content)
        return self.__parse_access_token_response(content)

    def __get_at_by_password(self, fqs):
        user_creds = self.__get_user_creds()
        client_creds = self.__get_client_creds()
        # URL encode fqs.
        encoded_fqs = quote(fqs.encode())
        auth_header = self.__get_auth_header(
            client_creds.get_credential_alias(), client_creds.get_secret())
        replaced = str.format(DefaultAccessTokenProvider._RO_GRANT_FORMAT,
                              user_creds.get_credential_alias(), encoded_fqs)
        # Build the actual payload to acquire access token.
        payload = replaced + user_creds.get_secret()
        return self.__get_access_token(auth_header, payload, fqs)

    def __get_at_by_psm_app(self):
        """
        Acquiring account access token using PSMApp provisioned by Oracle for
        each tenant. Keeping this path to remain the backward compatibility if
        users are using the client id and secret from Application ANDC. This
        path shouldn't work after IDCS hides client secret of Oracle-created
        Applications. This will be deprecated eventually.
        """
        # 1. acquire IDCS AT
        result = self.__get_at_by_password(
            DefaultAccessTokenProvider._IDCS_SCOPE)
        if result is None:
            raise IllegalStateException(
                'Error acquiring Identity Cloud Service access token, unable '
                + 'to get metadata to proceed acquiring account access token.')
        # 2. look up audience, client id and secret of PSMApp
        auth_header = 'Bearer ' + result
        psm_info = self.__get_psm_app(auth_header)
        if psm_info is None:
            raise IllegalStateException(
                'Error finding required metadata from Identity Cloud Service,'
                + ' unable to proceed acquiring account access token.')
        # 3. acquire PSM AT
        auth_header = self.__get_auth_header(psm_info.client_id,
                                             psm_info.client_secret)
        psm_fqs = psm_info.audience + Utils.PSM_SCOPE
        user_creds = self.__get_user_creds()
        replaced = str.format(DefaultAccessTokenProvider._RO_GRANT_FORMAT,
                              user_creds.get_credential_alias(), psm_fqs)
        payload = replaced + user_creds.get_secret()
        return self.__get_access_token(auth_header, payload, psm_fqs)

    def __get_auth_header(self, client_id, secret):
        # Return authorization header in form of 'Basic <clientId:secret>'.
        pair = client_id + ':' + secret
        try:
            return 'Basic ' + b64encode(pair)
        except TypeError:
            return 'Basic ' + b64encode(pair.encode()).decode()

    def __get_client_creds(self):
        creds = self.__creds_provider.get_oauth_client_credentials()
        if creds is None:
            raise IllegalArgumentException(
                'OAuth client credentials unavailable.')
        return creds

    def __get_credential_file(self, properties_file):
        creds_file = PropertiesCredentialsProvider.get_property_from_file(
            properties_file, DefaultAccessTokenProvider._CREDS_FILE_PROP)
        if creds_file is None:
            return PropertiesCredentialsProvider._DEFAULT_CREDS_FILE
        return creds_file

    def __get_entitlement_id(self, properties_file):
        return PropertiesCredentialsProvider.get_property_from_file(
            properties_file, DefaultAccessTokenProvider._ENTITLEMENT_ID_PROP)

    def __get_idcs_url(self, properties_file):
        # Methods used to fetch IDCS-related properties from given file.
        idcs_url = PropertiesCredentialsProvider.get_property_from_file(
            properties_file, DefaultAccessTokenProvider._IDCS_URL_PROP)
        if idcs_url is None:
            raise IllegalArgumentException(
                'Must specify IDCS URL in IDCS properties file.')
        return idcs_url

    def __get_psm_app(self, auth_header):
        """
        Get PSMApp metadata from IDCS. The secret of PSMApp will be hidden by
        IDCS, if no secret, return an error to ask users create custom client.
        This will be deprecated eventually.
        """
        # Get PSMApp metadata from IDCS.
        response = self.__request_utils.do_get_request(
            self.__idcs_url + Utils.APP_ENDPOINT +
            DefaultAccessTokenProvider._PSM_APP_FILTER,
            Utils.token_headers(self.__host, auth_header), self.__timeout_ms)
        if response is None:
            raise IllegalStateException(
                'Error getting required metadata from Identity Cloud Service,'
                + ' unable to acquire account access token, no response')
        response_code = response.get_status_code()
        content = response.get_content()
        if response_code >= codes.multiple_choices:
            Utils.handle_idcs_errors(
                response, 'Getting account metadata',
                'Please grant user Identity Domain Administrator or ' +
                'Application Administrator role')
        oauth_id = 'name'
        audience = 'audience'
        secret = 'clientSecret'
        try:
            oauth_id_value = Utils.get_field(content, oauth_id)
            audience_value = Utils.get_field(content, audience)
            secret_value = Utils.get_field(content, secret)
        except IllegalStateException as ise:
            raise UnauthorizedException(
                'Please grant user Identity Domain Administrator or ' +
                'Application Administrator role. ' + str(ise))
        if oauth_id_value is None or audience_value is None:
            raise IllegalStateException(
                'Account metadata response contains invalid value: ' + content)
        if secret_value is None:
            raise IllegalStateException(
                'Account metadata doesn\'t have a secret, unable to acquire ' +
                'account access token. Must create the custom OAuth Client ' +
                'first. Account metadata: ' + content)
        return DefaultAccessTokenProvider.PSMAppInfo(oauth_id_value,
                                                     secret_value,
                                                     audience_value)

    def __get_user_creds(self):
        user_creds = self.__creds_provider.get_user_credentials()
        if user_creds is None:
            raise IllegalArgumentException('User credentials unavailable.')
        return user_creds

    def __handle_token_error_response(self, response_code, content):
        if response_code >= codes.server_error:
            self.__logutils.log_info(
                'Error acquiring access token, expected to retry, error ' +
                'response: ' + content + ', status code: ' +
                str(response_code))
            raise RequestTimeoutException(
                'Error acquiring access token, expected to retry, error ' +
                'response: ' + content + ', status code: ' +
                str(response_code))
        elif response_code == codes.bad and content is None:
            # IDCS doesn't return error message in case of credentials has
            # invalid URL encoded characters.
            raise IllegalArgumentException(
                'Error acquiring access token, status code: ' +
                str(response_code) +
                ', CredentialsProvider supplies invalid credentials')
        else:
            raise InvalidAuthorizationException(
                'Error acquiring access token from Identity Cloud Service. ' +
                'IDCS error response: ' + content + ', status code: ' +
                str(response_code))

    def __is_credentials_provider(self, provider):
        if (provider is not None
                and not isinstance(provider, CredentialsProvider)):
            raise IllegalArgumentException('provider must be an instance of ' +
                                           'CredentialsProvider.')

    def __parse_access_token_response(self, response):
        """
        A valid response from IDCS is in JSON format and must contains the field
        "access_token" and "expires_in".
        """
        response = loads(response)
        access_token = response.get(DefaultAccessTokenProvider._AT_FIELD)
        if access_token is None:
            raise IllegalStateException(
                'Access token response contains invalid value, response: ' +
                str(response))
        self.__logutils.log_debug('Acquired access token ' + access_token)
        return access_token

    class PSMAppInfo:
        def __init__(self, client_id, client_secret, audience):
            self.client_id = client_id
            self.client_secret = client_secret
            self.audience = audience
コード例 #5
0
 def set_logger(self, logger):
     CheckValue.check_logger(logger, 'logger')
     self._logger = logger
     self._logutils = LogUtils(logger)
     self._request_utils = RequestUtils(self._sess, self._logutils)
     return self
コード例 #6
0
    def __init__(self,
                 provider=None,
                 config_file=None,
                 profile_name=None,
                 tenant_id=None,
                 user_id=None,
                 fingerprint=None,
                 private_key=None,
                 pass_phrase=None,
                 region=None,
                 duration_seconds=MAX_ENTRY_LIFE_TIME,
                 refresh_ahead=DEFAULT_REFRESH_AHEAD):
        """
        The SignatureProvider that generates and caches request signature.
        """
        #
        # This class depends on the oci package
        #
        if oci is None:
            raise ImportError('Package "oci" is required; please install.')

        CheckValue.check_int_gt_zero(duration_seconds, 'duration_seconds')
        CheckValue.check_int_gt_zero(refresh_ahead, 'refresh_ahead')
        if duration_seconds > SignatureProvider.MAX_ENTRY_LIFE_TIME:
            raise IllegalArgumentException(
                'Access token cannot be cached longer than ' +
                str(SignatureProvider.MAX_ENTRY_LIFE_TIME) + ' seconds.')

        self._region = None
        if provider is not None:
            if not isinstance(
                    provider,
                (oci.signer.Signer, oci.auth.signers.SecurityTokenSigner)):
                raise IllegalArgumentException(
                    'provider should be an instance of oci.signer.Signer or ' +
                    'oci.auth.signers.SecurityTokenSigner.')
            self._provider = provider
            try:
                region_id = provider.region
            except AttributeError:
                region_id = None
            if region_id is not None:
                self._region = Regions.from_region_id(region_id)
        elif (tenant_id is None or user_id is None or fingerprint is None
              or private_key is None):
            CheckValue.check_str(config_file, 'config_file', True)
            CheckValue.check_str(profile_name, 'profile_name', True)
            if config_file is None and profile_name is None:
                # Use default user profile and private key from default path of
                # configuration file ~/.oci/config.
                config = oci.config.from_file()
            elif config_file is None and profile_name is not None:
                # Use user profile with given profile name and private key from
                # default path of configuration file ~/.oci/config.
                config = oci.config.from_file(profile_name=profile_name)
            elif config_file is not None and profile_name is None:
                # Use user profile with default profile name and private key
                # from specified configuration file.
                config = oci.config.from_file(file_location=config_file)
            else:  # config_file is not None and profile_name is not None
                # Use user profile with given profile name and private key from
                # specified configuration file.
                config = oci.config.from_file(file_location=config_file,
                                              profile_name=profile_name)
            self._provider = oci.signer.Signer(config['tenancy'],
                                               config['user'],
                                               config['fingerprint'],
                                               config['key_file'],
                                               config.get('pass_phrase'),
                                               config.get('key_content'))
            region_id = config.get('region')
            if region_id is not None:
                self._provider.region = region_id
                self._region = Regions.from_region_id(region_id)
        else:
            CheckValue.check_str(tenant_id, 'tenant_id')
            CheckValue.check_str(user_id, 'user_id')
            CheckValue.check_str(fingerprint, 'fingerprint')
            CheckValue.check_str(private_key, 'private_key')
            CheckValue.check_str(pass_phrase, 'pass_phrase', True)
            if path.isfile(private_key):
                key_file = private_key
                key_content = None
            else:
                key_file = None
                key_content = private_key
            self._provider = oci.signer.Signer(tenant_id, user_id, fingerprint,
                                               key_file, pass_phrase,
                                               key_content)
            if region is not None:
                if not isinstance(region, Region):
                    raise IllegalArgumentException(
                        'region must be an instance of an instance of Region.')
                self._provider.region = region.get_region_id()
                self._region = region

        self._signature_cache = Memoize(duration_seconds)
        self._refresh_interval_s = (duration_seconds - refresh_ahead
                                    if duration_seconds > refresh_ahead else 0)

        # Refresh timer.
        self._timer = None
        self._service_url = None
        self._logger = None
        self._logutils = LogUtils()
        self._sess = Session()
        self._request_utils = RequestUtils(self._sess, self._logutils)
コード例 #7
0
class OAuthClient:
    """
    Utility to create a custom OAuth client.

    To connect and authenticate to the Oracle NoSQL Database Cloud Service, a
    client needs to acquire an access token from Oracle Identity Cloud Service
    (IDCS). As a prerequisite, a custom OAuth client named *NoSQLClient* must
    be created first using this utility. This custom client needs to be created
    only once for a tenant.

    This utility needs a valid access token in a token file that can be
    downloaded from the IDCS admin console. After logging into the IDCS admin
    console, choose *Applications* from the button on the top left. Find the
    Application named *ANDC*, click the button *Generate Access Token*, in the
    pop-up window, pick *Invoke Identity Cloud Service APIs* under
    *Customized Scopes*. Click on *Download Token* and a token file will be
    generated and downloaded. Note that this token has a lifetime of one hour.

    After the token file has been downloaded, run this utility to complete the
    OAuth Client creation:

    .. code-block:: shell

      python oauth_client.py -create -idcs_url <tenant-specific IDCS URL> \
-token <token file>

    The tenant-specific IDCS URL is the IDCS host assigned to the tenant. After
    logging into the IDCS admin console, copy the host of the IDCS admin console
    URL. For example, the format of the admin console URL is
    "https\://{tenantId}.identity.oraclecloud.com/ui/v1/adminconsole". The
    "https\://{tenantId}.identity.oraclecloud.com" portion is the required
    parameter.

    After creation, the utility will print out *NoSQLClient is created*.
    The OAuth client id and secret will also be printed out. A credentials file
    template *credentials.tmp* with client id an secret will be generated at the
    working directory by default. Use *-credsdir* to specify different directory.

    This utility also can be used to delete this custom OAuth client in case the
    creation process failed unexpectedly.

    .. code-block:: shell

      python oauth_client.py -delete -idcs_url <tenant-specific IDCS URL> \
-token <token file>

    In addition, this utility can be used to verify if OAuth client is
    configured properly, for example

    .. code-block:: shell

      python oauth_client.py -verify -idcs_url <tenant-specific IDCS URL> \
-token <token file>
    """
    #
    # NOTE: above is simple doc. This information is on the implementation.
    # This custom OAuth client must be created with a specified name. The client
    # must:
    # - enable password, client_credentials as allowed grants
    # - have PSM and NDCS fully-qualified scopes (FQS) as allowed scopes
    # - have ANDC_FullAccessRole
    #
    # The OAuth client creation steps are:
    # 1. Find PSM and NDCS primary audiences from IDCS
    # 2. Build PSM and NDCS FQS with primary audiences, put in the OAuth \
    # client JSON payload
    # 3. POST <idcs_url>/admin/v1/Apps with OAuth client JSON payload
    # 4. Find role ID of ANDC_FullAccessRole
    # 5. Grant ANDC_FullAccessRole to created custom OAuth client
    #

    # Default OAuth client name
    _DEFAULT_NAME = 'NoSQLClient'
    # Default credentials template file name
    _CREDS_TMP = 'credentials.temp'
    # Endpoint with filter used to get PSM App
    _PSM_APP_EP = (Utils.APP_ENDPOINT +
                   '?filter=serviceTypeURN+eq+%22PSMResourceTenatApp%22')
    # Endpoint with filter used to get ANDC App
    _ANDC_APP_EP = (Utils.APP_ENDPOINT + '?filter=serviceTypeURN+eq+%22' +
                    'ANDC_ServiceEntitlement%22+and+isOAuthResource+eq+true')
    # Endpoint with filter used to get role ID of ANDC_FullAccessRole
    _ANDC_ROLE_EP = (Utils.ROLE_ENDPOINT +
                     '?filter=displayName+eq+%22ANDC_FullAccessRole%22')
    # Endpoint with filter used to get oauth client
    _CLIENT_EP = Utils.APP_ENDPOINT + '?filter=displayName+eq+%22'
    # JSON used to create custom OAuth client
    _CLIENT = (
        '{{"displayName": "{0}","isOAuthClient": true,' +
        '"isOAuthResource": false,"isUnmanagedApp": true,"active": true,' +
        '"description": "Custom OAuth Client for application access to ' +
        'NoSQL Database Cloud Service","clientType": "confidential",' +
        '"allowedGrants": ["password", "client_credentials"]' +
        ',"trustScope": "Explicit","allowedScopes": [' +
        '{{"fqs": "{1}"}},{{"fqs": "{2}"}}],' +
        '"schemas": ["urn:ietf:params:scim:schemas:oracle:idcs:App"],' +
        '"basedOnTemplate": {{"value": "CustomWebAppTemplateId"}}}}')
    # JSON used to grant role to client
    _GRANT = (
        '{{"app": {{"value": "{0}"}},"entitlement": {{' +
        '"attributeName": "appRoles","attributeValue": "{1}"}},' +
        '"grantMechanism": "ADMINISTRATOR_TO_APP",' +
        '"grantee": {{"value": "{2}","type": "App"}},' +
        '"schemas": ["urn:ietf:params:scim:schemas:oracle:idcs:Grant"]}}')
    _DEACTIVATE = (
        '{"active": false,"schemas": [' +
        '"urn:ietf:params:scim:schemas:oracle:idcs:AppStatusChanger"]}')

    # Main argument flags
    _IDCS_URL_FLAG = '-idcs_url'
    _TOKEN_FILE_FLAG = '-token'
    _CREATE_FLAG = '-create'
    _DELETE_FLAG = '-delete'
    _VERIFY_FLAG = '-verify'
    _NAME_FLAG = '-name'
    _DIR_FLAG = '-credsdir'
    _TIMEOUT_FLAG = '-timeout'
    _VERBOSE_FLAG = '-verbose'

    def __init__(self):
        self.__parse_args()
        url = urlparse(self.__idcs_url)
        self.__host = url.hostname
        # logger used for HTTP request logging
        self.__logger = self.__get_logger()
        self.__logutils = LogUtils(self.__logger)
        self.__sess = Session()
        self.__request_utils = RequestUtils(self.__sess, self.__logutils)

    def execute_commands(self):
        try:
            if self.__delete:
                self.__do_delete()
            elif self.__create:
                self.__do_create()
            else:
                errors = list()
                self.__do_verify(errors)
                if len(errors) != 0:
                    print('Verification failed: ')
                    for err in errors:
                        print(err)
        except Exception:
            print(format_exc())
        finally:
            if self.__sess is not None:
                self.__sess.close()

    def __add_app(self, auth, payload):
        # Add the custom OAuth client
        response = self.__request_utils.do_post_request(
            self.__idcs_url + Utils.APP_ENDPOINT,
            Utils.scim_headers(self.__host, auth), payload, self.__timeout_ms)
        self.__check_not_none(response, 'response of adding OAuth client')
        response_code = response.get_status_code()
        content = response.get_content()
        if response_code == codes.conflict:
            raise IllegalStateException(
                'OAuth Client ' + self.__name + ' already exists. To ' +
                'recreate, run with ' + OAuthClient._DELETE_FLAG + '. To ' +
                'verify if existing client is configured correctly, run with ' +
                OAuthClient._VERIFY_FLAG)
        elif response_code >= codes.multiple_choices:
            OAuthClient.__idcs_errors(response, 'Adding custom client')
        app_id = 'id'
        oauth_id = 'name'
        secret = 'clientSecret'
        app_id_value = Utils.get_field(content, app_id)
        oauth_id_value = Utils.get_field(content, oauth_id)
        secret_value = Utils.get_field(content, secret)
        if (app_id_value is None or oauth_id_value is None or
                secret_value is None):
            raise IllegalStateException(
                str.format('Unable to find {0} or {1} or {2} in ,' + content,
                           app_id, oauth_id, secret))
        return OAuthClient.Client(app_id_value, oauth_id_value, secret_value)

    def __check_not_none(self, response, action):
        if response is None:
            raise IllegalStateException(
                'Error ' + action + ' from Oracle Identity Cloud Service, ' +
                'no response')

    def __creds_template(self, client_id, secret):
        file_dir = ((path.abspath(path.dirname(argv[0])) if
                     self.__temp_file_dir is None else
                     self.__temp_file_dir) + sep + OAuthClient._CREDS_TMP)
        if path.exists(file_dir):
            remove(file_dir)
        with open(file_dir, 'w') as f:
            if client_id is not None:
                f.write(PropertiesCredentialsProvider.CLIENT_ID_PROP + '=' +
                        client_id + '\n')
                f.write(PropertiesCredentialsProvider.CLIENT_SECRET_PROP + '=' +
                        secret + '\n')
            f.write(PropertiesCredentialsProvider.USER_NAME_PROP + '=\n')
            f.write(PropertiesCredentialsProvider.PWD_PROP + '=\n')
        return file_dir

    def __deactivate_app(self, auth, app_id):
        # Deactivate OAuth client
        response = self.__request_utils.do_put_request(
            self.__idcs_url + Utils.STATUS_ENDPOINT + sep + app_id,
            Utils.scim_headers(self.__host, auth), OAuthClient._DEACTIVATE,
            self.__timeout_ms)
        self.__check_not_none(
            response, 'response of deactivating OAuth client')
        if codes.ok <= response.get_status_code() < codes.multiple_choices:
            return
        OAuthClient.__idcs_errors(
            response, 'deactivating OAuth client ' + self.__name)

    def __do_create(self):
        self.__output('Creating OAuth Client ' + self.__name)
        try:
            # Find PSM and ANDC fqs
            auth = 'Bearer ' + self.__get_bootstrap_token()
            psm_fqs = self.__get_psm_audience(auth) + Utils.PSM_SCOPE
            andc = self.__get_andc_info(auth)
            andc_fqs = andc.audience + AccessTokenProvider.SCOPE
            self.__log_verbose('Found scopes ' + psm_fqs + ', ' + andc_fqs)
            # Add custom client
            add_app = OAuthClient._CLIENT.format(
                self.__name, psm_fqs, andc_fqs)
            client_info = self.__add_app(auth, add_app)
            self.__log_verbose('Added OAuth client ' + self.__name)
            # Find ANDC role id
            role_id = self.__get_id(
                auth, self.__idcs_url + OAuthClient._ANDC_ROLE_EP, 'role')
            self.__log_verbose('Found role id ' + role_id)
            # Grant ANDC_FullAccessRole to custom client
            grant = OAuthClient._GRANT.format(
                andc.app_id, role_id, client_info.app_id)
            self.__grant_role(auth, grant)
            self.__log_verbose('Granted role to OAuth client')
            self.__output(self.__name + ' is created\nClient ID: ' +
                          client_info.oauth_id + '\nClient secret: ' +
                          client_info.secret)
            creds_path = self.__creds_template(client_info.oauth_id,
                                               client_info.secret)
            self.__output('Credential template file ' + creds_path)
        except Exception as e:
            self.__output('Failed to create OAuth client ' + self.__name)
            raise e

    def __do_delete(self):
        self.__output('Deleting OAuth Client ' + self.__name)
        try:
            auth = 'Bearer ' + self.__get_bootstrap_token()
            # Find OAuth client AppId
            app_id = self.__get_id(
                auth, self.__idcs_url + OAuthClient._CLIENT_EP + self.__name +
                '%22', 'client')
            self.__log_verbose('Found OAuth client AppId: ' + app_id)
            # Deactivate the OAuth client
            self.__deactivate_app(auth, app_id)
            self.__log_verbose('OAuth client deactivated')
            # Remove the OAuth client
            self.__remove_client(auth, app_id)
            self.__output(self.__name + ' is deleted')
        except Exception as e:
            self.__output('Failed to remove OAuth client ' + self.__name)
            raise e

    def __do_verify(self, errors):
        self.__output('Verifying OAuth Client ' + self.__name)
        try:
            auth = 'Bearer ' + self.__get_bootstrap_token()
            response = self.__request_utils.do_get_request(
                self.__idcs_url + OAuthClient._CLIENT_EP + self.__name + '%22',
                Utils.scim_headers(self.__host, auth), self.__timeout_ms)
            self.__check_not_none(response, 'client metadata')
            response_code = response.get_status_code()
            content = response.get_content()
            if response_code >= codes.multiple_choices:
                OAuthClient.__idcs_errors(
                    response, 'Getting client ' + self.__name)
            grants = Utils.get_field(content, 'allowedGrants')
            if grants is None:
                # No results in response
                raise IllegalStateException(
                    'OAuth Client ' + self.__name + ' doesn\'t exist, or the ' +
                    'token file is invalid, user who downloads the token ' +
                    'must have Identity Domain Administrator role')
            # Verify if client has required grants
            self.__verify_grants(grants, errors)
            # Verify if client has PSM and ANDC FQS
            self.__verify_scopes(
                Utils.get_field(content, 'allowedScopes', 'fqs'), errors)
            # Verify if client has ANDC role
            self.__verify_role(
                Utils.get_field(content, 'grantedAppRoles', 'display'), errors)
            if len(errors) > 0:
                return
            self.__output('Verification succeed')
        except Exception as e:
            self.__output('Verification failed of OAuth client ' + self.__name)
            raise e

    def __get_andc_info(self, auth):
        # Get App ANDC metadata from IDCS
        response = self.__request_utils.do_get_request(
            self.__idcs_url + OAuthClient._ANDC_APP_EP,
            Utils.scim_headers(self.__host, auth), self.__timeout_ms)
        self.__check_not_none(response, 'getting service metadata')
        content = response.get_content()
        if response.get_status_code() >= codes.multiple_choices:
            OAuthClient.__idcs_errors(response, 'Getting service metadata')
        audience = 'audience'
        app_id = 'id'
        audience_value = Utils.get_field(content, audience)
        app_id_value = Utils.get_field(content, app_id)
        if audience_value is None or app_id_value is None:
            raise IllegalStateException(
                str.format('Unable to find {0} or {1} in ,' + content,
                           audience, app_id))
        return OAuthClient.ANDC(app_id_value, audience_value)

    def __get_bootstrap_token(self):
        # Read access token from given file
        with open(self.__at_file, 'r') as at_file:
            content = at_file.read()
        bootstrap_token = loads(content)
        field = 'app_access_token'
        app_access_token = bootstrap_token.get(field)
        if app_access_token is None:
            raise IllegalStateException(
                'Access token file contains invalid value: ' + content)
        return app_access_token

    def __get_id(self, auth, url, resource):
        response = self.__request_utils.do_get_request(
            url, Utils.scim_headers(self.__host, auth), self.__timeout_ms)
        self.__check_not_none(response, 'getting ' + resource + ' id')
        if response.get_status_code() >= codes.multiple_choices:
            OAuthClient.__idcs_errors(response, 'Getting id of ' + resource)
        return str(Utils.get_field(
            response.get_content(), 'id', allow_none=False))

    def __get_logger(self):
        """
        Returns the logger used for OAuthClient.
        """
        logger = getLogger(self.__class__.__name__)
        if self.__verbose:
            logger.setLevel(WARNING)
        else:
            logger.setLevel(INFO)
        log_dir = (path.abspath(path.dirname(argv[0])) + sep + 'logs')
        if not path.exists(log_dir):
            mkdir(log_dir)
        logger.addHandler(FileHandler(log_dir + sep + 'oauth.log'))
        return logger

    def __get_psm_audience(self, auth):
        response = self.__request_utils.do_get_request(
            self.__idcs_url + OAuthClient._PSM_APP_EP,
            Utils.scim_headers(self.__host, auth), self.__timeout_ms)
        self.__check_not_none(response, 'getting account metadata')
        if response.get_status_code() >= codes.multiple_choices:
            OAuthClient.__idcs_errors(response, 'Getting account metadata')
        return str(Utils.get_field(
            response.get_content(), 'audience', allow_none=False))

    def __grant_role(self, auth, payload):
        # Grant ANDC_FullAccessRole to OAuth client
        response = self.__request_utils.do_post_request(
            self.__idcs_url + Utils.GRANT_ENDPOINT,
            Utils.scim_headers(self.__host, auth), payload,
            self.__timeout_ms)
        self.__check_not_none(response, ' response of granting role')
        if codes.ok <= response.get_status_code() < codes.multiple_choices:
            return
        OAuthClient.__idcs_errors(response, 'Granting required role to client')

    def __log_verbose(self, msg):
        if self.__verbose:
            print(msg)

    def __output(self, msg):
        print(msg)

    def __parse_args(self):
        parser = ArgumentParser(prog='OAuthClient')
        parser.add_argument(
            OAuthClient._IDCS_URL_FLAG, required=True, help='The idcs_url.',
            metavar='<tenant-base IDCS URL>')
        parser.add_argument(
            OAuthClient._TOKEN_FILE_FLAG, required=True,
            help='The path of the token get from IDCS admin console.',
            metavar='<access token file path>')
        parser.add_argument(
            OAuthClient._NAME_FLAG, default=OAuthClient._DEFAULT_NAME,
            help='The OAuth Client name.',
            metavar='<client name> default: NoSQLClient')
        parser.add_argument(
            OAuthClient._DIR_FLAG,
            help='The directory for generating the credentials file template.',
            metavar=('<credentials template directory path> ' +
                     'default: current dir'))
        parser.add_argument(
            OAuthClient._TIMEOUT_FLAG, type=int,
            default=Utils.DEFAULT_TIMEOUT_MS, help='The timeout.',
            metavar='<request timeout> default: 12000 ms')
        parser.add_argument(
            OAuthClient._CREATE_FLAG, action='store_true',
            help='To create the OAuth Client.')
        parser.add_argument(
            OAuthClient._DELETE_FLAG, action='store_true',
            help='To delete the OAuth Client.')
        parser.add_argument(
            OAuthClient._VERIFY_FLAG, action='store_true',
            help='To verify the OAuth Client.')
        parser.add_argument(
            OAuthClient._VERBOSE_FLAG, action='store_true',
            help='To log verbose information.')

        args = parser.parse_args()
        self.__idcs_url = args.idcs_url
        self.__at_file = args.token
        self.__name = args.name
        self.__temp_file_dir = args.credsdir
        self.__timeout_ms = args.timeout
        self.__create = args.create
        self.__delete = args.delete
        self.__verify = args.verify
        self.__verbose = args.verbose

        if not (self.__create or self.__delete or self.__verify):
            parser.error(
                'Missing required argument ' + OAuthClient._CREATE_FLAG +
                ' | ' + OAuthClient._DELETE_FLAG + ' | ' +
                OAuthClient._VERIFY_FLAG)

    def __remove_client(self, auth, app_id):
        response = self.__request_utils.do_delete_request(
            self.__idcs_url + Utils.APP_ENDPOINT + sep + app_id,
            Utils.scim_headers(self.__host, auth), self.__timeout_ms)
        self.__check_not_none(response, 'response of deleting OAuth client')
        if codes.ok <= response.get_status_code() < codes.multiple_choices:
            return
        OAuthClient.__idcs_errors(
            response, 'removing OAuth client ' + self.__name)

    def __verify_grants(self, grants, errors):
        self.__log_verbose('OAuth client allowed grants: ' + str(grants))
        match = 0
        for grant in grants:
            if (grant.lower() == 'password' or
                    grant.lower() == 'client_credentials'):
                match += 1
        if match != 2:
            errors.append('Missing required allowed grants, require Resource ' +
                          'Owner and Client Credentials')
        self.__log_verbose('Grants verification succeed')

    def __verify_role(self, roles, errors):
        if roles is None:
            raise IllegalStateException(
                'OAuth client ' + self.__name + ' doesn\'t have roles')
        self.__log_verbose('OAuth client allowed roles: ' + str(roles))
        match = 0
        for role in roles:
            if role == 'ANDC_FullAccessRole':
                match += 1
        if match != 1:
            errors.append('Missing required role ANDC_FullAccessRole')
        self.__log_verbose('Role verification succeed')

    def __verify_scopes(self, fqs_list, errors):
        self.__log_verbose('OAuth client allowed scopes: ' + str(fqs_list))
        match = 0
        for fqs in fqs_list:
            if Utils.PSM_SCOPE in fqs or AccessTokenProvider.SCOPE in fqs:
                match += 1
        if match != 2:
            errors.append('Missing required OAuth scopes, client only have ' +
                          str(fqs_list))
        self.__log_verbose('Scope verification succeed')

    @staticmethod
    def __idcs_errors(response, action):
        Utils.handle_idcs_errors(
            response, action, ' Access token in the token file expired,' +
            ' or the token file is generated with incorrect scopes,' +
            ' requires Identity Domain Administrator')

    class ANDC:
        def __init__(self, app_id, audience):
            self.app_id = app_id
            self.audience = audience

    class Client:
        def __init__(self, app_id, oauth_id, secret):
            self.app_id = app_id
            self.oauth_id = oauth_id
            self.secret = secret