





# With pip:
pip install json
pip install requests
# With Anaconda:
conda install json
conda install requests


| KEY | VALUE |
| Authorization | Bearer <YOUR BEARER TOKEN> |
| x-sap-sac-custom-auth | true |
| x-csrf-token | fetch |



| KEY | VALUE |
| x-sap-sac-custom-auth | true |
| x-csrf-token | <YOUR CSRF TOKEN> |
| Content-Type | application/json |
| Authorization | Bearer <YOUR BEARER TOKEN> |
{
"schemas": ["ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],
"userName": "<ID>",
"name":
{
"givenName": "<FIRST NAME>",
"familyName": "<LAST NAME>"
},
"displayName": "<DISPLAY NAME>",
"emails": [
{
"value": "<WORK EMAIL>",
"type": "work",
"primary": "true"
}
],
"roles": [],
"groups": [],
"urn:scim:schemas:extension:enterprise:1.0": {
"manager": {
"managerId": ""
}
}
}
class UrlConstructor:
# Add the basic url for your SAC tenant:
# You should be able to find the base url from you SAC landing page:
# https://<THIS PART>/sap/fpa/ui/app.html#/home
sacBaseUrl = ""
def __init__(self):
print("URL constructor instansiated")
def __str__(self):
print("Helper class: Fetching URLs and tokens")
# Function for contructing request URLs.
# Returns the base url,
def fetchUrl(endpoint: Literal["bearer", "csrf", "users", "groups", "user", "group"], entity=""):
'''Returns a url & endpoint. Note: User & Group requires the optional "entity" format'''
if UrlConstructor.sacBaseUrl == "":
raise ValueError("Base URL not defined")
if endpoint == 'bearer':
# The bearer URL is retrived from SAC -> Settings -> App. Integration -> Client.
return str(HeaderConstructor.oAuth2SAMLTokenUrl + 'grant_type=client_credentials')
elif endpoint == 'csrf':
return str(UrlConstructor.sacBaseUrl + '/api/v1/scim/Users')
elif endpoint == 'users':
return str(UrlConstructor.sacBaseUrl + '/api/v1/scim/Users')
elif endpoint == 'groups':
return str(UrlConstructor.sacBaseUrl + '/api/v1/scim/Groups')
elif endpoint == 'user':
return str(UrlConstructor.sacBaseUrl + '/api/v1/scim/Users' + '/' + entity)
elif endpoint == 'group':
return str(UrlConstructor.sacBaseUrl + '/api/v1/scim/Groups' + '/' + entity)
else:
passclass HeaderConstructor:
# The bearer URL is retrived from SAC -> Settings -> App. Integration ->OAuth Clients.
# Note: Some guides you find online might use the 'Token URL'. This procuded errors when i tried.
oAuth2SAMLTokenUrl = ""
# Both Client ID and secret are retrived from SAC -> Settings -> App. Integration -> Client.
clientId = "" # <-- <INSERT CLIENT ID>
clientSecret = "" # <-- <ENTER CLIENT SECRET>
def __init__(self):
print("Header constructor instansiated")
def __str__(self):
print("Helper class: Constructing header information")
# Function for fetching the bearer token.
# Client ID and secret are generated by your SAC tenant.
# Follow the first step ("Configuration SAP Analytics Cloud") in this guide:
# http://www.sapspot.com/how-to-use-rest-api-in-sap-analytics-cloud-to-update-user-profile-in-embedded...,
# to set up the provisioning agent.
def getToken():
'''Function for fetching the bearer token'''
url = UrlConstructor.fetchUrl('bearer')
# Thanks to Stefan Himmelhuber: Blog post:
# https://blogs.sap.com/2020/02/06/sac-export-user-list-by-rest-api/
# for a clever encoding method 🙂
Auth = '{0}:{1}'.format(
HeaderConstructor.clientId, HeaderConstructor.clientSecret)
headers = {
'Authorization': 'Basic {0}'.format(
base64.b64encode(Auth.encode('utf-8')).decode('utf-8'))}
params = {'grant_type': 'client_credentials'}
tokenRequest = requests.post(url, headers=headers, params=params)
# HTTP ERROR Handling.
if not str(tokenRequest.status_code) == '200':
ErrorHandling.httpRequestError(
tokenRequest.status_code, "getToken", url, tokenRequest.json())
returnToken = tokenRequest.json()['access_token']
return returnToken
# Function for fetching the CSRF-token,
def getCsrfToken():
'''Returns the csrf token, for PUT and POST requests'''
# Getting the CSRF token.
url = UrlConstructor.fetchUrl('csrf')
headers = HeaderConstructor.getHeaders()
csrfRequest = requests.get(url, headers=headers)
if not str(csrfRequest.status_code) == '200':
ErrorHandling.httpRequestError(
csrfRequest.status_code, "getToken", url, csrfRequest.json())
return_payload = {}
return_payload['Token'] = csrfRequest.headers['x-csrf-token']
return_payload['Session'] = csrfRequest.headers['set-cookie']
return return_payload
def getHeaders(requestType="GET"):
'''Returns the custom header, needed for the different requests.
POST & PUT requests also generates the required the csrf-token'''
#Ini. dict
headers = {}
# Required constants for the SAC API.
headers['x-sap-sac-custom-auth'] = "true"
# Custom section:
headers['Authorization'] = 'Bearer ' + HeaderConstructor.getToken()
# Additions for the different types of calls.
# POST and PUT requires extra info; whereas GET only requires the Bearer.
if requestType == "POST" or requestType == "PUT":
csrfInfo = {}
csrfInfo = HeaderConstructor.getCsrfToken()
headers['Cookie'] = csrfInfo['Session']
headers['x-csrf-token'] = csrfInfo['Token']
headers['Content-Type'] = 'application/json'
else:
# Optional. dosen't hurt. Used in the GET call to fetch the token.
headers['x-csrf-token'] = 'fetch'
return headers
class UserManagement:
def __init__(self) -> None:
return "User management constructor instansiated"
def __str__(self) -> str:
return "Helper class: Manageing users and teams"
def getGroups(returnType: Literal["json", "custom"], selectEntities=[], teams=[]):
''' Returns all groups, now called Teams, from SAC, eigther as raw json or select entities.
Valid entities: "id", "displayName", "meta", "members", "roles"
Specify a team ID in teams, for query a subset of teams.
'''
urlList = []
groupList = []
# If list "Teams" was provided.
if teams:
for team in teams:
url = UrlConstructor.fetchUrl('group', entity=team)
urlList.append(url)
else:
url = UrlConstructor.fetchUrl('groups')
urlList.append(url)
# Get the request headers.
headers = HeaderConstructor.getHeaders()
if returnType == "custom":
entityList = []
MatchList = ["id", "displayname", "meta", "members", "roles"]
# Collect the wanted entities and weed out the bad ones
for entity in selectEntities:
# Validate the entity is in the match list, otherwise pass.
if not str(entity).lower() in MatchList:
continue # <-- Discard
entityList.append(entity)
# Loop through the list of URLs.
# For each url,
for url in urlList:
groupRequest = requests.get(url, headers=headers)
if not str(groupRequest.status_code) == '200':
ErrorHandling.httpRequestError(
groupRequest.status_code, "getToken", url, groupRequest.json())
if returnType == "json":
groupList.append(groupRequest.json())
if returnType == "custom":
customPayload = {}
groupRequestJson = groupRequest.json()
# Collect the desired data into groupList
for entity in entityList:
customPayload[entity] = groupRequestJson.get(entity)
groupList.append(customPayload)
return groupList
def getUsers(returnType: Literal["json", "custom"], selectEntities=[], users=[]):
''' Returns all users from SAC, eigther as raw json or select entities.
"userName", "id", "preferredLanguage", "meta", "name", "members", "roles",
"displayName", "active", "emails", "photos", "roles", "groups"
'''
urlList = []
userList = []
if users:
for user in users:
url = UrlConstructor.fetchUrl('user', entity=user)
urlList.append(url)
else:
url = UrlConstructor.fetchUrl('users')
urlList.append(url)
if returnType == "custom":
customPayload = {}
entityList = []
# Valid entities in the SAC user schema.
MatchList = [
"userName", "id", "preferredLanguage",
"meta", "name", "members", "roles",
"displayName", "active", "emails",
"photos", "roles", "groups"]
# Collect the wanted entities and weed out the bad ones
for entity in selectEntities:
# Validate the entity is in the match list, otherwise pass.
if not str(entity).lower() in MatchList:
continue # <-- Discards the entity.
entityList.append(entity)
headers = HeaderConstructor.getHeaders()
for url in urlList:
userRequest = requests.get(url, headers=headers).json()
if returnType == "json":
return userRequest.json()
if returnType == "custom":
# Collect the desired data into groupList
for user in userRequest["Resources"]:
for entity in entityList:
customPayload[entity] = user.get(entity)
userList.append(customPayload)
return userList
def assignPrivileges(userId, roles=[], teams=[]):
if not roles and not teams:
raise ValueError("No roles or teams provided")
url = UrlConstructor.fetchUrl('user', userId)
headers = HeaderConstructor.getHeaders("PUT")
# Construct body:
# Fetch the body of requests, in order to make changes.
userBodyRequest = requests.get(url, headers=headers)
if not str(userBodyRequest.status_code) == '200':
ErrorHandling.httpRequestError(
userBodyRequest.status_code, "getToken", url, userBodyRequest.json())
userBody = userBodyRequest.json()
# Manipulate request body.
userBody["roles"] = roles
userBody["groups"] = teams
putAssign = requests.put(url, headers=headers,
data=json.dumps(userBody))
if not str(putAssign.status_code) == '200':
ErrorHandling.httpRequestError(
putAssign.status_code, "getToken", url, putAssign.json())
return putAssign.json()
def createUser(userName, familyName, emails,
firstName="", roles=[], teams=[], managerId=""):
# NOTE: Users, not a single user.
url = UrlConstructor.fetchUrl('users')
headers = HeaderConstructor.getHeaders("POST")
userBody = BodyConstructor.getRequestBody('create user')
# Format Teams
teamsBody = []
# Format teams into correct SCHEMA.
for team in teams:
templateBody = BodyConstructor.getRequestBody('add team')
templateBody["value"] = str(team).upper()
templateBody["$ref"] = "/api/v1/scim/Groups/" + str(team).upper()
teamsBody.append(templateBody)
# Non required Body elements that can be manipulated:
displayName = firstName + ' ' + familyName # <-- Display name*
# Assigning custom values to the
userBody["userName"] = userName
userBody["name"]["firstName"] = firstName
userBody["name"]["familyName"] = familyName
userBody["displayName"] = displayName
userBody["emails"][0]["value"] = emails
userBody["urn:scim:schemas:extension:enterprise:1.0"]["manager"]["managerId"] = managerId
userBody["roles"] = roles
userBody["groups"] = teamsBody
postCreate = requests.post(
url, headers=headers, data=json.dumps(userBody))
return postCreate.json()
def createTeam(teamId, teamTxt, members=[], roles=[]):
url = UrlConstructor.fetchUrl('groups')
headers = HeaderConstructor.getHeaders('POST')
teamBody = BodyConstructor.getRequestBody('create team')
memberBody = []
# Format members into correct SCHEMA.
for user in members:
templateBody = BodyConstructor.getRequestBody('add user')
templateBody["value"] = str(user).upper()
templateBody["$ref"] = "/api/v1/scim/Users/" + str(user).upper()
memberBody.append(templateBody)
teamBody["id"] = teamId
teamBody["displayName"] = teamTxt
teamBody["members"] = members
teamBody["roles"] = roles
postCreate = requests.post(
url, headers=headers, data=json.dumps(teamBody))
return postCreate.json()
def updateUser(userName, familyName, emails,
firstName="", roles=[], teams=[], managerId=""):
url = UrlConstructor.fetchUrl('user', entity=userName)
headers = HeaderConstructor.getHeaders("PUT")
userBody = BodyConstructor.getRequestBody('create user')
# Format Teams
teamsBody = []
# Format teams into correct SCHEMA.
for team in teams:
templateBody = BodyConstructor.getRequestBody('add team')
templateBody["value"] = str(team).upper()
templateBody["$ref"] = "/api/v1/scim/Groups/" + str(team).upper()
teamsBody.append(templateBody)
# Non required Body elements that can be manipulated:
displayName = firstName + ' ' + familyName # <-- Display name*
# Assigning custom values to the
userBody["userName"] = userName
userBody["name"]["firstName"] = firstName
userBody["name"]["familyName"] = familyName
userBody["name"]["displayName"] = displayName
userBody["emails"]["value"] = emails
userBody["urn:scim:schemas:extension:enterprise:1.0"]["manager"]["managerId"] = managerId
userBody["roles"] = roles
userBody["Teams"] = teamsBody
postCreate = requests.put(
url, headers=headers, data=json.dumps(userBody))
return postCreate.json()
def updateTeam(teamId, teamTxt, members=[], roles=[]):
url = UrlConstructor.fetchUrl('group', entity=teamId)
headers = HeaderConstructor.getHeaders('PUT')
teamBody = BodyConstructor.getRequestBody('create team')
memberBody = []
# Format members into correct SCHEMA.
for user in members:
templateBody = BodyConstructor.getRequestBody('add user')
templateBody["value"] = str(user).upper()
templateBody["$ref"] = "/api/v1/scim/Users/" + str(user).upper()
memberBody.append(templateBody)
teamBody["teamId"] = teamId
teamBody["dislpayName"] = teamTxt
teamBody["members"] = members
teamBody["roles"] = roles
postCreate = requests.put(
url, headers=headers, data=json.dumps(teamBody))
return postCreate.json()





import SAC#Returns all teams, as json.
print("Raw json:")
teams = SAC.UserManagement.getGroups('json')
print(teams)
#Returns all teams, but only the team text and the assigned users
print("Custom list:")
teamsTwoColumn = SAC.UserManagement.getGroups('custom',selectEntities=['displayName','members'])
print(teamsTwoColumn)

#Create a user, with statis values.
userRequest = SAC.UserManagement.createUser("<ID>", "Sorensen", "<EMAIL>","Chris")
#Assign roles to the user:
privRequest = SAC.UserManagement.assignPrivileges("<ID>", roles=["PROFILE:sap.epm:Viewer", "PROFILE:t.S:cso_test"])

SAC.UserManagement.createTeam(teamId="ROLES_FOLDER",teamTxt="Team for containing all roles",roles=[])
allRoles = SAC.UserManagement.getGroups('custom', selectEntities= ["roles"], teams= ["ROLES_FOLDER"])
print(allRoles)[{'roles': ['PROFILE:sap.epm:Application_Creator', 'PROFILE:sap.epm:BI_Admin', 'PROFILE:sap.epm:BI_Content_Creator']}]You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
| User | Count |
|---|---|
| 40 | |
| 31 | |
| 23 | |
| 20 | |
| 19 | |
| 16 | |
| 16 | |
| 10 | |
| 10 | |
| 9 |