Skip to content

Supporting Client Secret rotation programatically + documentation update #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 139 additions & 2 deletions aepp/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import os
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add unit tests to this PR based on our new convention with all features

from pathlib import Path
from typing import Optional
import json
import json, time

# Non standard libraries
from .config import config_object, header, endpoints
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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:
"""
Expand All @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use python style conventions in functions, namely with underscores for functions

Copy link
Contributor Author

@pitchmuc pitchmuc Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have agreed but the whole package is using the camelCase notation.
I would think that we should be consistent over the different modules and keep camelCase usage.
It does not hurt in the end.

Also changing the method names is a massive breaking change.

"""
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/<orgId>/<projectId>/credentials/<credentialId>/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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With these exceptions what are the expectations on how the user recovers, what do they need to do

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rotating client capability are not supported for JWT or Oauth V1.
There is no recovery, except moving to OauthV2 credential type.
How do you recommend to provide that feedback ?

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/<orgId>/<projectId>/credentials/<credentialId>/details/oauthservertoserver
"""
if self.connectionType != "oauthV2":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we move these checks into Utils instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what part you are referring about.
the configs part ? We can, we just need to change the reference everywhere.
If it is in configs.py vs utils.py it does not matter.
Let me know.
I would believe that it would be a different PR to be honest.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to do any validations on res?

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/<orgId>/<projectId>/credentials/<credentialId>/details/oauthservertoserver
"""
if self.connectionType != "oauthV2":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This set of if statements looks very similar to the above function, lets move these to a generic if statement that checks the item and returns the appropriate error message

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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this endpoint be the same for stage and prod?

Copy link
Contributor Author

@pitchmuc pitchmuc Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I am only able to follow what is actually providing on the official documentation. https://developer.adobe.com/developer-console/docs/guides/authentication/ServerToServerAuthentication/implementation/

I do not think that you are using OauthV2, but Oauth V1.
I would suggest that we go live so provide clients with this functionality and if you ever need it and it is required to have a different url, we can use a variable to modify the endpoint automatically.
We can even use the environment parameter from the configuration file or configure method to define the correct endpoint if it turns out to be different from the stage and prod.

res = self.connector.postData(endpoint,headers=myheader)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to do any validations before we operate on the contents of the results

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets rename to delete_secret

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As explained above, the package current naming convention is using lower camelCase, so I think we should keep it consistent.
Otherwise, we would need to rename all existing methods which is a massive breaking change.

"""
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/<orgId>/<projectId>/credentials/<credentialId>/details/oauthservertoserver
"""
if self.connectionType != "oauthV2":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my comments above on these if statements

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
1 change: 1 addition & 0 deletions aepp/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import json
import re
from .configs import ConnectObject
from .catalog import ObservableSchemaManager

json_extend = [
{
Expand Down
Loading