diff --git a/aepp/configs.py b/aepp/configs.py index 8c876d6..639a1ce 100644 --- a/aepp/configs.py +++ b/aepp/configs.py @@ -12,7 +12,7 @@ import os from pathlib import Path from typing import Optional -import json +import json, time # Non standard libraries from .config import config_object, header, endpoints @@ -320,6 +320,7 @@ def __init__(self, self.sandbox = sandbox self.scopes = scopes self.token = "" + self.auth_code = auth_code self.__configObject__ = { "org_id": self.org_id, "client_id": self.client_id, @@ -334,7 +335,8 @@ def __init__(self, "jwtTokenEndpoint" : self.jwtEndpoint, "oauthTokenEndpointV1" : self.oauthEndpointV1, "oauthTokenEndpointV2" : self.oauthEndpointV2, - "scopes": self.scopes + "scopes": self.scopes, + "auth_code": self.auth_code } def connect(self)->None: @@ -344,6 +346,7 @@ def connect(self)->None: self.connector = connector.AdobeRequest(self.__configObject__,self.header) self.token = self.connector.token self.header['Authorization'] = 'bearer '+self.token + self.connectionType = self.connector.connectionType def getConfigObject(self)->dict: """ @@ -367,3 +370,137 @@ def setSandbox(self,sandbox:str=None)->dict: self.header["x-sandbox-name"] = sandbox self.__configObject__["sandbox"] = sandbox return self.getConfigObject() + + def setOauthV2setup(self,credentialId:str=None,orgDevId:str=None)->bool: + """ + set the credential ID and the OrgIdDev as attributes of the instance. + * credentialId + * orgDevId + Argument: + credentialId : OPTIONAL : The credential id that can be found on your Project page. + orgDevId : OPTIONAL : the org Id but NOT the IMS string. It is defined on your project page. + Example : https://developer.adobe.com/console/projects///credentials//details/oauthservertoserver + """ + if self.connectionType != "oauthV2": + raise Exception("You are trying to set credential ID or orgDevId for auth that is not OauthV2. We do not support these auth type.") + if credentialId is None: + raise ValueError("credentialId is None") + if orgDevId is None: + raise ValueError("orgDevId is None") + self.credentialId = credentialId + self.orgDevId = orgDevId + + def getSecrets(self,credentialId:str=None,orgDevId:str=None)->dict: + """ + Access the different token available for your client ID. + If you did not use the setOauthV2setup, you can pass the required information as parameters. + Arguments: + credentialId : OPTIONAL : The credential id that can be found on your Project page. + orgDevId : OPTIONAL : the org Id but NOT the IMS string. It is defined on your project page. + Example : https://developer.adobe.com/console/projects///credentials//details/oauthservertoserver + """ + if self.connectionType != "oauthV2": + raise Exception("You are trying to use a service that is only supportede for OauthV2 authen. We do not support the other auth types.") + if credentialId is None and self.credentialId is None: + raise ValueError("You are not providing the credential ID and did not use the setOauthV2setup method.\n Use it to prepare this method") + if orgDevId is None and self.orgDevId is None: + raise ValueError("You are not providing the orgDevId and did not use the setOauthV2setup method.\n Use it to prepare this method") + if credentialId is None and self.credentialId is not None: + credentialId = self.credentialId + if orgDevId is None and self.orgDevId is not None: + orgDevId = self.orgDevId + if self.token is None: + raise Exception("You need to generate a token by using the connect method first") + myheader = { + 'Authorization' : 'Bearer '+self.token, + 'x-api-key' : self.client_id + } + endpoint = f"https://api.adobe.io/console/organizations/{orgDevId}/credentials/{credentialId}/secrets" + res = self.connector.getData(endpoint,headers=myheader) + return res + + def createSecret(self,credentialId:str=None,orgDevId:str=None)->dict: + """ + Create a new secret with a new token for Oauth V2 credentials. + If you did not use the setOauthV2setup, you can pass the required information as parameters. + ATTENTION : In order to use it, you will need to have added the I/O Management API to your project. + Returns the new token and new secret that is automatically being used for that connection. + Arguments + credentialId : OPTIONAL : The credential id that can be found on your Project page. + orgDevId : OPTIONAL : the org Id but NOT the IMS string. It is defined on your project page. + Example : https://developer.adobe.com/console/projects///credentials//details/oauthservertoserver + """ + if self.connectionType != "oauthV2": + raise Exception("You are trying to use a service that is only supportede for OauthV2 authen. We do not support the other auth types.") + if credentialId is None and self.credentialId is None: + raise ValueError("You are not providing the credential ID and did not use the setOauthV2setup method.\n Use it to prepare this method") + if orgDevId is None and self.orgDevId is None: + raise ValueError("You are not providing the orgDevId and did not use the setOauthV2setup method.\n Use it to prepare this method") + if credentialId is None and self.credentialId is not None: + credentialId = self.credentialId + if orgDevId is None and self.orgDevId is not None: + orgDevId = self.orgDevId + if self.token is None: + raise Exception("You need to generate a token by using the connect method first") + myheader = { + 'Authorization' : 'Bearer '+self.token, + 'x-api-key' : self.client_id + } + endpoint = f"https://api.adobe.io/console/organizations/{orgDevId}/credentials/{credentialId}/secrets" + res = self.connector.postData(endpoint,headers=myheader) + if 'client_secret' not in res.keys(): + raise Exception("Could not find a client_secret in the key") + self.secret = res['client_secret'] + self.__configObject__['secret'] = res['client_secret'] + self.connector.config['secret'] = res['client_secret'] + return res + + def updateConfigFile(self,destination:str=None)->None: + """ + Once creating a client secret, you would need to update your config file with your new secret. + Arguments: + destination : REQUIRED : Destination path of the file name to updated. + """ + if self.connectionType != 'OauthV2': + raise Exception('Do not support update for non Oauth Server to Server type') + json_data: dict = { + "org_id": self.org_id, + "client_id": self.client_id, + "secret": self.secret, + "sandbox-name": self.sandbox, + "scopes": self.scopes, + "environment": "prod" + } + with open(destination, "w") as cf: + cf.write(json.dumps(json_data, indent=4)) + + def deleteSecrete(self,secretUID:str=None,credentialId:str=None,orgDevId:str=None,)->None: + """ + Delete an old token from your different token accessed + Arguments: + secretUID : REQUIRED : The token to delete + credentialId : OPTIONAL : The credential id that can be found on your Project page. + orgDevId : OPTIONAL : the org Id but NOT the IMS string. It is defined on your project page. + Example : https://developer.adobe.com/console/projects///credentials//details/oauthservertoserver + """ + if self.connectionType != "oauthV2": + raise Exception("You are trying to use a service that is only supportede for OauthV2 authen. We do not support the other auth types.") + if credentialId is None and self.credentialId is None: + raise ValueError("You are not providing the credential ID and did not use the setOauthV2setup method.\n Use it to prepare this method") + if orgDevId is None and self.orgDevId is None: + raise ValueError("You are not providing the orgDevId and did not use the setOauthV2setup method.\n Use it to prepare this method") + if credentialId is None and self.credentialId is not None: + credentialId = self.credentialId + if orgDevId is None and self.orgDevId is not None: + orgDevId = self.orgDevId + if self.token is None: + raise Exception("You need to generate a token by using the connect method first") + if secretUID is None: + raise ValueError("You need to pass a correct value for the tokenUID") + endpoint = f"https://api.adobe.io/console/organizations/{orgDevId}/credentials/{credentialId}/secrets/{secretUID}/" + myheader = { + 'Authorization' : 'Bearer '+self.token, + 'x-api-key' : self.client_id + } + res = self.connector.deleteData(endpoint,headers=myheader) + return res \ No newline at end of file diff --git a/aepp/schema.py b/aepp/schema.py index e9abcae..dd5d75b 100644 --- a/aepp/schema.py +++ b/aepp/schema.py @@ -20,6 +20,7 @@ import json import re from .configs import ConnectObject +from .catalog import ObservableSchemaManager json_extend = [ { diff --git a/docs/connectObject.md b/docs/connectObject.md new file mode 100644 index 0000000..7791fb7 --- /dev/null +++ b/docs/connectObject.md @@ -0,0 +1,258 @@ +# Connect Object class + +This documentation is aiming at providing detail overview of the `ConnectInstance` parameter that is available from the `importConfigFile` method or the `configure` method.\ +This parameter leverage the `ConnectObject` class that is available in the configs module of `aepp`. + + +## Connect Object origin + +The `ConnectObject` class origin comes from the possibility to handle multiple sandboxes within AEP via different connections.\ +The previous capability of aepp requires you to always load the latest configuration of your sandbox when you wanted to create an instance for that different sandbox, even though the API credentials were the same and only the sandbox name setup was different.\ +In order to simplify the move between sandboxes with the `aepp` wrapper, the idea was to keep the configuration parameter for each sandbox in its own class, that is independent from any other submodule of `aepp`, such as `schema`, `queryservice`, etc...\ +This submodule can be used in the same manner for all submodule so you can better control which sandbox you are connecting to, even with the same config file. + +## Connect Object creation + +### Via importConfigFile + +You can create a new instance of the ConnectObject class by passing `True` to the `ConnectInstance` parameter during the `importConfigFile` method. +The sandbox selection can be done in 2 different places: + +* You have different config file per sandbox +* You overwrite the sandbox in the config file during the import via the `sandbox` parameter. + +Examples: + +```python +import aepp + +aepp.importConfigFile('myconfig.json') ## do NOT create an instance +mySandbox1 = aepp.importConfigFile('myconfig.sandbox1.json',connectInstance=True) ## create an instance for sandbox 1 +mySandbox2 = aepp.importConfigFile('myconfig.sandbox2.json',connectInstance=True) ## create an instance for sandbox 2 +mySandbox3 = aepp.importConfigFile('myconfig.sandbox1.json',connectInstance=True,sandbox='sandbox3') ## create an instance for sandbox 3 +``` + +### Via configure method + +The `configure` method also provides a parameter to create an instance.\ +The main difference is that you need to pass all of the information for your connection directly as parameters.\ + +Examples: + +```python +import aepp + +aepp.configure(org_id=XXX,tech_id=YYYY,....) ## do NOT create an instance +mySandbox1 = aepp.configure(org_id=XXX,tech_id=YYYY,sandbox='sandbox1',...,connectInstance=True) ## create an instance for sandbox 1 +``` + +### Via ConnectObject class + +You can also create an instance directly from the `ConnectObject` class.\ +In that case, you would also need to pass all the different information requires for that instantiation.\ +The different parameter required for generating a `ConnectObject` class are: + +* org_id : REQUIRED : The organization IMS ID +* tech_id : REQUIRED : The Tech ID is available on the developer project. +* secret : REQUIRED : The secret that is available on the developer project. +* client_id : REQUIRED : The client ID is related to your developer project. +* path_to_key : OPTIONAL : In case you are doing JWT authentication, you would need to pass the path to the file containing the private.key. +* private_key : OPTIONAL : In case you are doing JWT authentication, you can pass the private.key content directly as a string here. +* scopes : OPTIONAL : In case you are doing Oauth V2, you would need to pass the scopes available on your developer project. +* sandbox : OPTIONAL : You can setup the sandbox name you want to use. Default with "prod" +* environment: OPTIONAL : Intended for adobe developer if they want to overwrite the endpoint to staging environment. Default 'prod' +* auth_code : OPTIONAL : Intended for internal Adobe service, in case they are using the Oauth V1 token. + +## Using the instance of connectObject + +Once you have an instance of the `connectObject`, you can use it in all of the different other sub modules and associated classes, by using `config` parameter.\ +Example for the `schema` module with the `Schema` classes + +```python +import aepp +mySandbox1 = aepp.importConfigFile('myconfig.sandbox1.json',connectInstance=True) ## create an instance for sandbox 1 +mySandbox2 = aepp.importConfigFile('myconfig.sandbox2.json',connectInstance=True) ## create an instance for sandbox 2 +mySandbox3 = aepp.importConfigFile('myconfig.sandbox1.json',connectInstance=True,sandbox='sandbox3') ## create an instance for sandbox 3 + +from aepp import schema + +schemaSandbox1 = schema.Schema(config=mySandbox1) +schemaSandbox2 = schema.Schema(config=mySandbox2) +schemaSandbox3 = schema.Schema(config=mySandbox3) + +``` + +## ConnectObject methods + +Once you have created a `connectObject` instance, you can then use some of the methods described below: + +### Connect + +The `connect` method generates a token and provide a connector instance in that class.\ +After that method is being used, there will be a `token`, `sandbox`, `connectionType` and `header` attributes available on the instance, such as (in the case `mySandbox1` is the instance): + +* `mySandbox1.token` provides the token that can be used for requests +* `mySandbox1.header` provides the header that can be used for requests +* `mySandbox1.sandbox` provides the information about the connected sandbox +* `mysandbox1.connectionType` provides the type of connection used, either `OauthV2`, `OauthV1` or `jwt` + +Example: + +```python +import aepp +mySandbox1 = aepp.importConfigFile('myconfig.sandbox1.json',connectInstance=True) + +mySandbox1.connect() + +mySandbox1.connectionType ## return the appropriate value, default `OauthV2` + +``` + +### Connector methods + +Once you have use the `connect` method, you can use the follwing methods: + +### getConfigObject + +The method will return the config object expected.\ +The config object will return the following information, if available: + +* "org_id" +* "client_id" +* "tech_id" +* "pathToKey" +* "private_key" +* "secret" +* "date_limit" +* "sandbox" +* "token" +* "imsEndpoint" +* "jwtTokenEndpoint" +* "oauthTokenEndpointV1" +* "oauthTokenEndpointV2" +* "scopes" +* "auth_code" + +### getConfigHeader + +It will return the header that can be used for the requests, such as: + +```JSON +{"Accept": "application/json", +"Content-Type": "application/json", +"Authorization": "....", +"x-api-key": "client_id", +"x-gw-ims-org-id": "org_id", +"x-sandbox-name": "sandbox" +} +``` + +### setSandbox + +This can take an argument that would change the sandbox associated with the instance.\ +You need to pass a sandbox name as a parameter.\ +This will replace the sandbox setup in the header and for the attribute provided. + +Example: + +```python +import aepp +mySandbox1 = aepp.importConfigFile('myconfig.sandbox1.json',connectInstance=True) + +mySandbox1.setSandbox('sandbox2') ## Now the mySandbox1 instance will be connected to sandbox 2 + +``` + +## Using the instance after the connection + +Once you have connected your instance with the Adobe API, you can use the `connector` object with the following methods: + +* `getData` +* `postData` +* `putData` +* `patchData` +* `deleteData` + +Example: + +```python +mySandbox1 = aepp.importConfigFile('myconfig.sandbox1.json',connectInstance=True) + +mySandbox1.connect() + +mySandbox1.connector.getData('endpoint',params={'key':'value'}) ## return the json response. + +``` + + +## Rotating Client Secret Programatically + +If you have used the `Oauth Server to Server` type of connection, you can also add the `I/O Management API` to your project in the project to manage the token you have on your Oauth setup. + +Only the `Oauth Server to Server` configuration type and adding the API mention above will make the following method working. + +**tip**: If you add the `I/O Management API` afterward, you would need to adjust the scopes. + +### setOauthV2setup + +This method is the recommended approach before using the other methods described below.\ +It will save the credentialId and the orgDevId into attribute of your instance. +It takes these 2 values as parameter. + +You can find both information, on your project page, when looking at the Oauth Server to Server page, on the developer.adobe.com website.\ +Such as : + +Example: + +```python +import aepp +mySandbox1 = aepp.importConfigFile('myconfig.sandbox1.json',connectInstance=True) + +mySandbox1.setOauthV2setup('credentialIdValue','orgDevIdValue') + +mySandbox1.credentialId ## returns credentialIdValue +mySandbox1.orgDevId ## returns orgDevIdValue +``` + +### getSecrets + +Access the different available token from your client ID. +It takes 2 optional parameters, in case you did not use the `setOauthV2setup` method: in case you did not use the `setOauthV2setup` method: + +* credentialId : OPTIONAL : The ID of your Project.(if you did not use the `setOauthV2setup` method) +* orgDevId : OPTIONAL : The Organzation ID from the developer console. It is NOT the same Id than the IMS org Id. (if you did not use the `setOauthV2setup` method) + + +### createSecret + +This method allows you to generate a secret token directly from the API.\ +It takes 2 optional paremeters, in case you did not use the `setOauthV2setup` method: + +* credentialId : OPTIONAL : The ID of your Project.(if you did not use the `setOauthV2setup` method) +* orgDevId : OPTIONAL : The Organzation ID from the developer console. It is NOT the same Id than the IMS org Id. (if you did not use the `setOauthV2setup` method) + +You can find both information, on your project page, when looking at the Oauth Server to Server page, on the developer.adobe.com website.\ +Such as : + +**Important** +The instance will automatically switch its setting to use the latest client secret for generating a new token. +**You will need to update the config file if you are using one. You can use the `updateConfigFile` method** + +### updateConfigFile + +After creating a new secret, the connection will automatically switch to the latest secret created.\ +However, your config file, if you have used one, is still having the old secret written. +If you wish to uupdate your config file, you can use that method, that will save the config file with the existing configuration options. +Arguments: + + * destination : REQUIRED : The path (optional) with the filename to be created. + +### deleteSecrete + +This will delete a token from the existing tokens available for this Oauth Server to Server setup.\ +It takes 1 required parameter and 2 optional parameters. +Arguments: + +* tokenUID : REQUIRED : The secret uuid to deleted +* credentialId : OPTIONAL : The ID of your Project.(if you did not use the `setOauthV2setup` method) +* orgDevId : OPTIONAL : The Organzation ID from the developer console. It is NOT the same Id than the IMS org Id. (if you did not use the `setOauthV2setup` method) \ No newline at end of file diff --git a/tests/connectInstance_test.py b/tests/connectInstance_test.py new file mode 100644 index 0000000..6c7e551 --- /dev/null +++ b/tests/connectInstance_test.py @@ -0,0 +1,78 @@ +# Copyright 2023 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. + +import unittest +from aepp import configs,ConnectObject +from unittest.mock import patch, MagicMock, Mock, ANY + +@patch("aepp.configs.importConfigFile") +def test_importConfigFile_Oauth(mock_data): + mock_data.return_value = { + "org_id": "", + "client_id": "", + "secret": "", + "sandbox-name": "prod", + "environment": "prod", + "scopes":"scopes" + } + assert(type(mock_data.return_value),dict) + + +class ConnectInstanceTest(unittest.TestCase): + + @patch("aepp.connector.AdobeRequest") + def test_instanceCreation(self,mock_connector): + mock_data = { + "org_id": "orgID", + "client_id": "client_id", + "secret": "YourSecret", + "sandbox-name": "prod", + "environment": "prod", + "scopes":"scopes" + } + instance_conn = mock_connector.return_value + instance_conn.postData.return_value = {'foo'} + self.mock_ConnectObject = ConnectObject(config=mock_data) + self.mock_ConnectObject.connect() + self.assertIsNotNone(self.mock_ConnectObject.connectionType) + + @patch("aepp.connector.AdobeRequest") + def test_setOauthV2Setup(self,mock_connector): + mock_data = { + "org_id": "orgID", + "client_id": "client_id", + "secret": "YourSecret", + "sandbox-name": "prod", + "environment": "prod", + "scopes":"scopes" + } + self.mock_ConnectObject = ConnectObject(config=mock_data) + self.mock_ConnectObject.connect() + self.mock_ConnectObject.connectionType = 'oauthV2' + self.mock_ConnectObject.setOauthV2setup(MagicMock(),MagicMock()) + assert(self.mock_ConnectObject.credentialId is not None) + assert(self.mock_ConnectObject.orgDevId is not None) + + + @patch("aepp.connector.AdobeRequest") + def test_setOauthV2Setup(self,mock_connector): + mock_data = { + "org_id": "orgID", + "client_id": "client_id", + "secret": "YourSecret", + "sandbox-name": "prod", + "environment": "prod", + "scopes":"scopes" + } + self.mock_ConnectObject = ConnectObject(config=mock_data) + self.mock_ConnectObject.connect() + with self.assertRaises(Exception) as cm: + self.mock_ConnectObject.setOauthV2setup(MagicMock(),MagicMock()) + self.assertEqual('You are trying to set credential ID or orgDevId for auth that is not OauthV2. We do not support these auth type.', str(cm.exception)) \ No newline at end of file