diff --git a/build/python/akamai_functions.py b/build/python/akamai_functions.py
new file mode 100644
index 0000000..f15edb4
--- /dev/null
+++ b/build/python/akamai_functions.py
@@ -0,0 +1,551 @@
+from akamai.edgegrid import EdgeGridAuth
+from urllib.parse import urljoin
+from datetime import datetime, timedelta
+import requests, json
+import re
+import time
+import names
+from flask import current_app
+
+# Control Center Account is 'Marketplace Test01'
+contract_id = 'ctr_V-41DUHPB'
+
+# admin user API credential
+client_secret = 'HP299+k2YnnyRKtbzKYtGOrM4ve1tXlrsn6o3ZZ/Mdw='
+host = 'akab-hkgndwdao42uuh4y-q4itpussnn3gck4x.luna.akamaiapis.net'
+access_token = 'akab-oaav5wopp546rowg-gjkaonxqkb6suzo7'
+client_token = 'akab-4daj4uu4qpqiukly-5kbvowcutmvydqk2'
+baseurl = 'https://' + host
+
+# unused hours of user
+unused_hours = 5
+
+sess = requests.Session()
+sess.auth = EdgeGridAuth(
+ client_token=client_token,
+ client_secret=client_secret,
+ access_token=access_token
+)
+
+
+def get_all_users():
+ response = sess.get(urljoin(
+ baseurl, '/identity-management/v3/user-admin/ui-identities?authGrants=true'))
+ s_code = response.status_code
+ body = response.json()
+ users = body
+ return users
+
+def verify_user_assigned_ever(user, firstName, lastName):
+ is_user_assigned_ever = False
+ if user['firstName'] == firstName and user['lastName'] == lastName:
+ is_user_assigned_ever = True
+ return is_user_assigned_ever
+
+def verify_user_email_domain(user):
+ pattern = r'^[a-zA-Z0-9_.+-]+@akamai-lab\.com$'
+ is_correct_domain = False
+
+ email = user['email']
+ if re.fullmatch(pattern, email):
+ is_correct_domain = True
+ return is_correct_domain
+
+def verify_user_lastLoginDate(user, hours):
+ is_valid = False
+ if 'lastLoginDate' in user: # if user has ever logged in to Control Center
+ date = user['lastLoginDate']
+ loginDate = datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ")
+ # current_app.logger.debug('lastLoginDate='+date)
+ currentDate = datetime.now()
+ # current_app.logger.debug('now='+str(currentDate))
+ difference = (currentDate - loginDate).total_seconds() / 3600
+ difference = int(difference)
+ # current_app.logger.debug('hoursSinceLastLogin='+str(difference)+' hours')
+ if difference > hours: # if user has not logged in longer than unusedDays
+ is_valid = True
+ else: # user has never logged in to Control Center yet
+ is_valid = True
+ return is_valid
+
+def select_user(users, hours, firstName, lastName):
+ selectedUser = None
+ current_app.logger.debug('hours='+str(hours))
+
+ # To avoid user pool exhaustion, we reuse existing users if they remain in the pool.
+ for user in users:
+ if verify_user_assigned_ever(user, firstName, lastName):
+ if verify_user_lastLoginDate(user, hours) is False:
+ selectedUser = user
+ break
+ # if there is no existing user in the pool, we search for a random available user
+ if selectedUser == None:
+ for user in users:
+ if verify_user_email_domain(user) and verify_user_lastLoginDate(user, hours):
+ uiIdentityId = user['uiIdentityId']
+ selectedUser = update_user(uiIdentityId, firstName, lastName)
+ break
+
+ if selectedUser != None:
+ current_app.logger.debug('selectedUser= %s', selectedUser)
+ else:
+ raise Exception('cannot find an available user')
+ # we might want to create a new group and a user here???
+ return selectedUser
+
+def reset_user_pwd(u_id):
+ response = sess.post(urljoin(baseurl, '/identity-management/v3/user-admin/ui-identities/' +
+ u_id+'/reset-password?sendEmail=false'))
+ pwd = response.json()['newPassword']
+ return pwd
+
+def update_user(uiIdentityId, firstName, lastName):
+ requestBody = {
+ "firstName": "John",
+ "lastName": "Doe",
+ "country": "USA",
+ "phone": "3456788765",
+ "contactType": "Billing",
+ "preferredLanguage": "English",
+ "sessionTimeOut": 64800,
+ "timeZone": "GMT"
+ }
+
+ current_app.logger.debug('uiIdentityId= %s', uiIdentityId)
+ requestBody['firstName'] = firstName
+ requestBody['lastName'] = lastName
+ current_app.logger.debug('requestBody= %s', requestBody)
+
+ response = sess.put(urljoin(baseurl, '/identity-management/v3/user-admin/ui-identities/' +
+ uiIdentityId + '/basic-info'), json=requestBody)
+ s_code = response.status_code
+ current_app.logger.debug('update_user status code= %s', s_code)
+ body = response.json()
+ current_app.logger.debug('update_user response body= %s', body)
+
+ return body
+
+def find_credential(authorizedUser, file_path):
+ credential =[]
+ file = open(file_path)
+ clients = json.load(file)
+ current_app.logger.debug(f'clients= {clients}')
+ for client in clients:
+ user = client["authorizedUser"]
+ if user == authorizedUser:
+ credential = client["credentials"]
+ break
+
+ if len(credential) > 0 :
+ print(f'credentials= {credential}')
+ return credential
+ else:
+ return None
+
+def create_credential(credential):
+ # this credential belongs to the selected user
+ client_secret = credential["client_secret"]
+ host = credential["host"]
+ access_token = credential["access_token"]
+ client_token = credential["client_token"]
+ baseurl = "https://" + host
+
+ new_credential={
+ "client_secret": "",
+ "host": "",
+ "access_token": "",
+ "client_token": ""
+ }
+ new_credential["host"] = host
+ new_credential["access_token"] = access_token
+
+ sess = requests.Session()
+ sess.auth = EdgeGridAuth(
+ client_token=client_token,
+ client_secret=client_secret,
+ access_token=access_token
+ )
+ headers = { "Accept":"application/json"}
+
+ # create a new credential for the selected user
+ response = sess.post(urljoin(baseurl, "/identity-management/v3/api-clients/self/credentials"), headers=headers)
+ s_code = response.status_code
+ current_app.logger.debug(f"status code= {s_code}")
+ if s_code == 201:
+ body = response.json()
+ current_app.logger.debug(f"response body= {body}")
+ new_credential["client_secret"] = body["clientSecret"]
+ new_credential["client_token"] = body["clientToken"]
+ credentialId = body["credentialId"]
+ current_app.logger.info(f"new credentialId= {credentialId}")
+
+ # if a new credential is created, start updating its expiration
+ payload = {
+ "expiresOn": "2020-10-11T23:06:59.000Z",
+ "status": "ACTIVE",
+ "description": "Expiration 4 hours"
+ }
+
+ current = datetime.now()
+ current_app.logger.debug (f"current= {current}")
+ expiration = current + timedelta(hours = 4)
+ current_app.logger.debug(f"expiration= {expiration}")
+ payload["expiresOn"] = expiration.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+ current_app.logger.debug(f"payload= {payload}")
+
+ path = "/identity-management/v3/api-clients/self/credentials/{}".format(credentialId)
+ headers = {
+ "Content-Type": "application/json",
+ "Accept": "application/json"}
+
+ # update the new credential expiration
+ response = sess.put(urljoin(baseurl, path), headers=headers, json=payload)
+ s_code = response.status_code
+ current_app.logger.debug(f"status code= {s_code}")
+
+ if s_code == 200:
+ body = response.json()
+ current_app.logger.debug(f"update credential response= {body}")
+ return new_credential
+ else:
+ return None
+ else:
+ return None
+
+#########################################################
+# The following functions are for administrator only!!! #
+#########################################################
+
+def is_user_in_users(users, uiUserName):
+ is_user_in_users = False
+ users = get_all_users()
+ for user in users:
+ if user['uiUserName'] == uiUserName:
+ is_user_in_users = True
+ current_app.logger.debug('user %s exists in all users', uiUserName)
+ break
+ if is_user_in_users == False:
+ current_app.logger.debug(
+ 'cannot find user %s in all users', uiUserName)
+ return is_user_in_users
+
+#########################################################
+# functions for deleting old properties #
+#########################################################
+
+
+def get_properties_by_group(group_id):
+ if isinstance(group_id, str) == False:
+ group_id = str(group_id)
+
+ response = sess.get(urljoin(
+ baseurl, '/papi/v1/properties?contractId='+contract_id+'&groupId='+group_id))
+ properties = response.json()
+ return properties
+
+# this function is used by delete_properties(properties)
+def get_property_details(property_id, contract_id, group_id):
+ response = sess.get(urljoin(baseurl, '/papi/v1/properties/' +
+ property_id+'?contractId='+contract_id+'&groupId='+group_id))
+ s_code = response.status_code
+ property_details = response.json()
+ if s_code != 200:
+ current_app.logger.debug(
+ 'failed to check status of property '+property_id)
+ current_app.logger.debug(property_details)
+ else:
+ current_app.logger.debug('checked status of property '+property_id)
+ return property_details
+
+
+def deactivate_property(property_id, network, property_version):
+ deactivate_payload = {
+ "acknowledgeAllWarnings": False,
+ "activationType": "DEACTIVATE",
+ "fastPush": True,
+ "ignoreHttpErrors": True,
+ "notifyEmails": ["learn@akamai.com"],
+ "useFastFallback": False,
+ "network": "STAGING",
+ "propertyVersion": 0
+ }
+ activation_link = None
+ if network == 'STAGING':
+ deactivate_payload['network'] = 'STAGING'
+ elif network == 'PRODUCTION':
+ deactivate_payload['network'] = 'PRODUCTION'
+ else:
+ current_app.abort(
+ 500, description="deactivation target network value is invalid >> network= "+network)
+
+ current_app.logger.debug('deactivation target network is ' +
+ str(deactivate_payload['network']))
+ deactivate_payload['propertyVersion'] = property_version
+ current_app.logger.debug('property_version= '+str(deactivate_payload['propertyVersion'])
+ )
+ response = sess.post(urljoin(baseurl, '/papi/v1/properties/' +
+ property_id+'/activations'), json=deactivate_payload)
+ s_code = response.status_code
+ body = response.json()
+ current_app.logger.debug('deactivation response= '+str(body))
+ activation_link = str(body['activationLink'])
+ if s_code != 201:
+ current_app.logger.debug('failed to deactivate property '+property_id)
+ current_app.logger.debug(body)
+ else:
+ current_app.logger.debug('started deactivation of property ' +
+ property_id + ' with activationLink '+activation_link)
+ return activation_link
+
+
+def delete_properties(properties):
+ deleted = False
+
+ staging_version = None
+ production_version = None
+
+ if len(properties['properties']['items']) > 0:
+ for property in properties['properties']['items']:
+ current_app.logger.debug('property= ' + str(property))
+ property_id = property['propertyId']
+ group_id = property['groupId']
+ current_app.logger.debug('property_id= '+property_id)
+ current_app.logger.debug('contract_id= '+contract_id)
+ current_app.logger.debug('group_id= '+group_id)
+
+ # Check property activation status
+ property_details = get_property_details(
+ property_id, contract_id, group_id)
+ current_app.logger.debug(
+ 'property_details= '+str(property_details))
+
+ # Deactivate property if it is active in staging or network
+ staging_version = property_details['properties']['items'][0]['stagingVersion']
+ if staging_version != 'None' and isinstance(staging_version, int):
+ current_app.logger.debug('property '+property_id+' version ' +
+ str(staging_version)+' is active in staging network')
+ activation_link = deactivate_property(
+ property_id, 'STAGING', staging_version)
+ if activation_link != None:
+ current_app.logger.info(
+ 'deactivation started in staging network. we will wait until it finishes')
+ while True:
+ time.sleep(1)
+ response = sess.get(urljoin(baseurl, activation_link))
+ s_code = response.status_code
+ body = response.json()
+ current_app.logger.debug(
+ 'get activation response code= '+str(s_code))
+ current_app.logger.debug(
+ 'get activation response body= '+str(body))
+
+ status = str(body['activations']['items'][0]['status'])
+ activation_id = str(
+ response['activations']['items'][0]['activationId'])
+ current_app.logger.debug('activation_id ' +
+ activation_id+' is '+status)
+ if status == 'DEACTIVATED':
+ current_app.logger.info(
+ 'finished deactivation of property '+property_id + ' in staging network')
+ break
+ else:
+ current_app.logger.debug(
+ 'deactivation is in progress. We will check activation status 1 second later')
+ else:
+ current_app.logger.debug('property '+property_id +
+ ' is not active in staging network')
+
+ # Deactivate property if it is active in production or network
+ production_version = property_details['properties']['items'][0]['productionVersion']
+ if production_version != 'None' and isinstance(production_version, int):
+ current_app.logger.debug('property '+property_id+' version ' +
+ str(production_version)+' is active in production network')
+ activation_link = deactivate_property(
+ property_id, 'PRODUCTION', production_version)
+ if activation_link != None:
+ current_app.logger.info(
+ 'deactivation started in production network. we will wait until it finishes')
+ while True:
+ time.sleep(1)
+ response = sess.get(urljoin(baseurl, activation_link))
+ body = response.json()
+ current_app.logger.debug(
+ 'get activation response code= '+str(s_code))
+ current_app.logger.debug(
+ 'get activation response body= '+str(body))
+ status = str(body['activations']['items'][0]['status'])
+ activation_id = str(
+ body['activations']['items'][0]['activationId'])
+ current_app.logger.debug('activation_id ' +
+ activation_id+' is '+status)
+ if status == 'DEACTIVATED':
+ current_app.logger.info(
+ 'finished deactivation of property '+property_id)
+ break
+ else:
+ current_app.logger.debug(
+ 'deactivation is in progress. We will check activation status again 1 second later')
+ else:
+ current_app.logger.debug('property '+property_id +
+ ' is not active in production network')
+
+ # Delete properties
+ response = sess.delete(
+ urljoin(baseurl, '/papi/v1/properties/'+property_id))
+ s_code = response.status_code
+ body = response.json()
+ current_app.logger.debug('delete status code= '+str(s_code))
+ if s_code != 200:
+ current_app.logger.debug(
+ 'failed to delete property '+property_id)
+ current_app.logger.debug(body)
+ deleted = False
+ break
+ else:
+ deleted = True
+ current_app.logger.debug('deleted property '+property_id)
+
+ else:
+ current_app.logger.debug('cannot find any property')
+
+ return deleted
+
+######################################################
+# functions for creating groups and users #
+######################################################
+
+
+def create_users(num):
+ new_users = []
+ for i in range(num):
+ group = create_group()
+ groupId = group['groupId']
+ groupName = group['groupName']
+ uiUserName = groupName + '@akamai-lab.com'
+ user = create_user(groupId, groupName)
+ if user != None:
+ current_app.logger.debug('user '+uiUserName+' is created')
+ new_users.append(user)
+ else:
+ current_app.logger.debug('cannot create a new user')
+ if len(new_users) > 0:
+ return new_users
+ else:
+ return None
+
+
+def create_group():
+ parent_groupId = '232397'
+ groupName = str(datetime.now().strftime('%Y%m%d%H%M%S'))
+ current_app.logger.debug('groupName= %s', groupName)
+ requestBody = {"groupName": groupName}
+ response = sess.post(urljoin(
+ baseurl, '/identity-management/v3/user-admin/groups/'+parent_groupId), json=requestBody)
+ s_code = response.status_code
+ current_app.logger.debug(
+ 'create_group status code= '+str(s_code))
+ body = response.json()
+ current_app.logger.debug('create_group response body= %s', body)
+ groupId = body['groupId']
+
+ if s_code == 201:
+ return body
+ else:
+ return 'cannot create a new group ' + str(groupName)
+
+
+def create_user(groupId, groupName):
+ headers = {"accept": "application/json"}
+ requestBody = {
+ "authGrants": [
+ {
+ # 3 = admin, 928 = editor. refer to 'list_roles.json' for more information.
+ "roleId": 928,
+ "groupId": 12345
+ }
+ ],
+ "firstName": "John",
+ "lastName": "Doe",
+ "email": "@akamai-lab.com",
+ "phone": "(123) 321-1234",
+ "additionalAuthentication": "NONE",
+ "country": "USA"
+ }
+
+ # firstName = names.get_first_name()
+ # lastName = names.get_last_name()
+ firstName = 'Not'
+ lastName = 'Assigned'
+ email = groupName + '@akamai-lab.com'
+ current_app.logger.debug('email= '+email)
+ requestBody['firstName'] = firstName
+ requestBody['lastName'] = lastName
+ requestBody['email'] = email
+ requestBody['authGrants'][0]['groupId'] = groupId
+ current_app.logger.debug('create_user request body= %s', requestBody)
+
+ response = sess.post(urljoin(
+ baseurl, '/identity-management/v3/user-admin/ui-identities?sendEmail=false'), json=requestBody, headers=headers)
+ s_code = response.status_code
+ current_app.logger.debug('create_user status code= %s', s_code)
+ body = response.json()
+ current_app.logger.debug('create_user response body= %s', body)
+
+ if s_code == 201:
+ return body
+ else:
+ return None
+
+#######################################################
+
+
+def list_roles():
+ response = sess.get(urljoin(
+ baseurl, '/identity-management/v3/user-admin/roles'))
+ s_code = response.status_code
+ current_app.logger.debug('list_roles status code= %s', s_code)
+ body = response.json()
+ current_app.logger.debug('list_roles response body= %s', body)
+
+ if s_code == 200:
+ return body
+ else:
+ return 'cannot list roles'
+
+
+def get_groups(unused_hours):
+ pattern = r'^[a-zA-Z0-9_.+-]+@akamai-lab\.com$'
+ current_app.logger.debug('unused_hours='+str(unused_hours))
+
+ groups = []
+ users = get_all_users()
+ filtered_users = [{}]
+ for user in users:
+ # current_app.logger.debug('user= ' + str(user))
+ email = user['email']
+ current_app.logger.debug('email= '+email)
+ is_correct_user = False
+ if re.fullmatch(pattern, email): # if email domain is akamai-lab.com
+ if 'lastLoginDate' in user: # if the user has ever logged in to Control Center
+ date = user['lastLoginDate']
+ loginDate = datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ")
+ current_app.logger.debug('lastLoginDate='+date)
+ currentDate = datetime.now()
+ current_app.logger.debug('now='+str(currentDate))
+ difference = (currentDate - loginDate).total_seconds() / 3600
+ current_app.logger.debug(
+ 'hoursSinceLastLogin=' + str(difference)+'hour(s)')
+ if difference > unused_hours: # if the user has not logged in longer than unused hours
+ is_correct_user = True
+ else: # if the user has never logged in
+ is_correct_user = True
+
+ if is_correct_user:
+ groupId = user['authGrants'][0]['groupId']
+ groupName = user['authGrants'][0]['groupName']
+ group = {"groupName": "", "groupId": ""}
+ group['groupName'] = groupName
+ group['groupId'] = groupId
+ groups.append(group)
+
+ return groups
\ No newline at end of file
diff --git a/build/python/app.py b/build/python/app.py
new file mode 100644
index 0000000..e83db5f
--- /dev/null
+++ b/build/python/app.py
@@ -0,0 +1,337 @@
+from flask import Flask, request, make_response, abort, jsonify, render_template, send_file
+from akamai.edgegrid import EdgeGridAuth
+from urllib.parse import urljoin
+from datetime import datetime, timedelta
+import requests
+import time
+import subprocess
+import logging
+import re
+import json
+import akamai_functions, docebo_functions, coder_functions
+from threading import Thread
+import randomname
+from io import StringIO
+import os
+
+app = Flask(__name__)
+app.logger.setLevel(logging.DEBUG)
+
+# How can we update coder admin_token??? What is the maximum expiration days?
+# coder server --max-token-lifetime. Default 2540400 hours = 290 days
+# The maximum lifetime duration users can specify when creating an API token.
+# coder tokens create --lifetime. Default 720 hours = 30 days
+# Specify a duration for the lifetime of the token.
+# Create token API key - curl -X POST http://coder-server:8080/api/v2/users/{user}/keys/tokens
+# {
+# "lifetime": 0,
+# "scope": "all",
+# "token_name": "string"
+# }
+
+admin_token = 'gNmzL1TLeN-gXfs7q10uWPINqtlpt02Pj'
+template_id = 'a5577a86-e700-41be-997b-6001d78061d4'
+user_email = 'learn@akamai.com'
+user_pwd = 'Qodnwk=$s8'
+
+token_header = {'Coder-Session-Token': admin_token,
+ 'Accept': 'application/json'}
+token_param = {'email': user_email, 'password': user_pwd}
+
+# unused hours of workspace
+unused_hours = 2
+
+# workspace ttl ms
+ttl = 86400000 # 24 hours
+
+###################################################
+# Control Center API functions start #
+###################################################
+
+@app.route('/flask/cc_download')
+def cc_download():
+ firstName = randomname.get_name()
+ lastName = randomname.get_name()
+ app.logger.debug('firstName= %s, lastName= %s', firstName, lastName)
+
+ app.logger.debug('get_users starts')
+ users = akamai_functions.get_all_users()
+ app.logger.debug('get_users ends')
+
+ app.logger.debug('select_user starts')
+ user = akamai_functions.select_user(users, unused_hours, firstName, lastName)
+ uiIdentityId = user['uiIdentityId']
+ app.logger.debug('select_user ends. uiIdentityId= %s', uiIdentityId)
+
+ app.logger.debug('reset_user_pwd starts')
+ pwd = akamai_functions.reset_user_pwd(uiIdentityId)
+ app.logger.debug('reset_user_pwd ends')
+
+ authorizedUser = user['uiUserName']
+ app.logger.debug(f'authorizedUser= {authorizedUser}')
+ credential = akamai_functions.find_credential(authorizedUser, '/home/akamai/dev/learnakamai/json/api_clients.json')
+ if credential is not None:
+ new_credential = akamai_functions.create_credential(credential)
+ if new_credential is not None:
+ credential = new_credential
+ else:
+ abort(500, description="cannot create new credential")
+ else:
+ abort(500, description="cannot find credential")
+
+ email = user['email']
+ if email != None and pwd != None:
+ authorizedUser = user['uiUserName']
+
+ app.logger.debug(f'credential= {credential}')
+ result = '[Control Center]\nLogin URL = https://control.akamai.com\n' + 'email = '+ email + '\npassword = '+ pwd + '\n\n'
+ result = result + '[API Credential]\nclient_secret = ' + credential["client_secret"] + '\nhost = '+ credential["host"] + '\naccess_token = ' + credential["access_token"] + '\nclient_token = ' + credential["client_token"]
+ app.logger.debug(f'result = '+result)
+
+
+ buffer = StringIO()
+ buffer.write(result)
+
+ response = make_response(buffer.getvalue())
+ response.headers['Content-Disposition'] = 'attachment; filename=credential.txt'
+ response.mimetype = 'application/octet-stream'
+
+ # return send_file(buffer, as_attachment=True, download_name="credential.txt", mimetype="html/text")
+
+ # response = make_response('
-- API credential --
client_secret = '+ credential["client_secret"] + '
host = '+ credential["host"] + '
access_token = ' + credential["access_token"] + 'client_token = ' + credential["client_token"])
+ return response
+ else:
+ abort(500, description="cannot find an available user")
+ return user
+
+# this function is the target url of iframe widget
+@app.route('/flask/credential')
+def credential():
+ return render_template('playground.html')
+
+# this function is called by playground.html javascript
+@app.route('/flask/a')
+def a():
+ docebo_user_id = request.args.get('user_id')
+ docebo_access_token = request.args.get('access_token')
+
+ app.logger.debug('get_users starts')
+ users = akamai_functions.get_all_users()
+ app.logger.debug('get_users ends')
+
+ app.logger.debug('docebo_get_users stars')
+ if docebo_user_id != None and docebo_access_token != None:
+ docebo_user = docebo_functions.docebo_get_user(docebo_user_id, docebo_access_token)
+ if docebo_user == None:
+ return 'cannot get Learn Akamai userdata of '+ docebo_user_id
+ else:
+ return 'docebo userdata is required'
+ firstName = docebo_user['first_name']
+ lastName = docebo_user['last_name']
+ app.logger.debug('docebo_get_users ends. firstName= %s, lastName= %s', firstName, lastName)
+
+ app.logger.debug('select_user starts')
+ user = akamai_functions.select_user(users, unused_hours, firstName, lastName)
+ uiIdentityId = user['uiIdentityId']
+ app.logger.debug('select_user ends. uiIdentityId= %s', uiIdentityId)
+
+ app.logger.debug('reset_user_pwd starts')
+ pwd = akamai_functions.reset_user_pwd(uiIdentityId)
+ app.logger.debug('reset_user_pwd ends')
+
+ authorizedUser = user['uiUserName']
+ app.logger.debug(f'authorizedUser= {authorizedUser}')
+ credential = akamai_functions.find_credential(authorizedUser, '/home/akamai/dev/learnakamai/json/api_clients.json')
+ if credential is not None:
+ new_credential = akamai_functions.create_credential(credential)
+ if new_credential is not None:
+ credential = new_credential
+ else:
+ abort(500, description="cannot create new credential")
+ else:
+ abort(500, description="cannot find credential")
+
+ email = user['email']
+ if email != None and pwd != None:
+ authorizedUser = user['uiUserName']
+
+ app.logger.debug(f'credential= {credential}')
+ response = make_response('-- API credential --
client_secret = '+ credential["client_secret"] + '
host = '+ credential["host"] + '
access_token = ' + credential["access_token"] + 'client_token = ' + credential["client_token"])
+ return response
+ else:
+ abort(500, description="cannot find an available user")
+ return user
+
+#########################################################################################
+# this function deletes properties in groups that have not been used for unused_hours-1 #
+# this function will be updated to run repeatedly #
+#########################################################################################
+@app.route('/flask/purge_groups')
+def purge_groups():
+ groups = akamai_functions.get_groups(unused_hours-1)
+ app.logger.debug('groups= '+str(groups))
+ messages = []
+ msg = None
+ for group in groups:
+ groupId = group['groupId']
+ groupName = group['groupName']
+ properties = akamai_functions.get_properties_by_group(groupId)
+ # if there is any existing properties in group
+ if properties['properties']['items']:
+ msg = 'found properties in group '+groupName
+ messages.append(msg)
+ app.logger.info(msg)
+ app.logger.debug('properties='+str(properties))
+
+ if akamai_functions.delete_properties(properties):
+ msg = 'deleted existing properties in group '+groupName
+ messages.append(msg)
+ app.logger.info(msg)
+ else:
+ abort(500, description="cannot delete properties in group "+groupName)
+ else:
+ msg = 'cannot find any property in group '+groupName
+ messages.append(msg)
+ app.logger.info(msg)
+ if bool(messages):
+ return messages
+ else:
+ abort(500, description="cannot purge groups")
+
+# test creating users
+@app.route('/flask/c')
+def c():
+ num = int(request.args.get('num'))
+ if num == None: num = 1
+ app.logger.debug('we will create %s user(s)', num)
+ new_users = akamai_functions.create_users(num)
+ if new_users != None:
+ return new_users
+ else:
+ return 'cannot create users'
+
+# Check list roles to assign users to right roles
+@app.route('/flask/list_roles')
+def d():
+ result = akamai_functions.list_roles()
+ return result
+###################################################
+# Control Center API functions end #
+###################################################
+
+
+
+
+
+###################################################
+# coder API functions start #
+###################################################
+
+
+# this function is the target URL of iframe widget
+@app.route('/flask/lab', methods=['GET'])
+def lab():
+ # return render_template('image_render.html', image=file)
+ return render_template('workspace.html')
+
+#@app.route('/flask/lab', methods=['GET'])
+#def lab():
+# user_id = request.args.get('user_id')
+# access_token = request.args.get('access_token')
+# html_body = 'Click To Launch Lab'
+# response = make_response(html_body)
+# return response
+
+# this function is called by workspace.html javascript
+@app.route('/flask/init', methods=['GET'])
+def init_workspace():
+ workspaces = coder_functions.list_workspaces('a')
+ workspace = coder_functions.search_workspaces(workspaces, unused_hours)
+
+ workspace_name = workspace['name']
+ workspace_status = workspace['latest_build']['status']
+ code_url = 'https://code--main--' + workspace_name + \
+ '--a.b.akamai-lab.com?folder=/home/coder/workspaces'
+ origin_url = 'https://origin--main--' + workspace_name + '--a.b.akamai-lab.com'
+ app.logger.debug('code_url= '+code_url)
+ app.logger.debug('origin_url= '+origin_url)
+
+ # 1st validation.
+ # if the selected_workspace is not running, we start it.
+ if workspace_status != 'running':
+ if coder_functions.start_workspace(workspace_name):
+ app.logger.debug('started workspace %s', workspace_name)
+ else:
+ abort(500, description="cannot start workspace " + workspace_name)
+
+ # 2nd validation.
+ # if the selected_workspace agent is not connected, we wait for it to become connected status
+ # if we started the selected_workspace above, this takes around 30-40 seconds
+ if coder_functions.is_agent_connected(workspace_name):
+ # if validations are successful, we create a session token for end-user to connect to code server and origin server.
+ # session_token = create_token(workspace_name)
+ # if session_token != '':
+ # 3rd validation.
+ # two apps in the workspace should be ready
+ if coder_functions.is_app_ready(code_url) and coder_functions.is_app_ready(origin_url):
+ app.logger.info(workspace_name + ': code and origin are ready')
+ response = make_response('')
+ # response.set_cookie("coder_session_token", session_token, domain='b.akamai-lab.com')
+ return response
+ #else:
+ # abort(500, description="cannot create session_token for " + workspace_name)
+ else:
+ abort(500, description="cannot connect to the workspace " + workspace_name)
+
+############################################################################################
+
+# delete workspaces by status. we can use this to delete all 'failed' workspaces, for example.
+# pending, starting, running, stopping, failed, canceling, canceled, deleting, deleted
+@app.route('/flask/delete_workspaces_by_status', methods=['GET'])
+def delete_workspaces_by_status():
+ target_status = request.args.get('status')
+ workspaces = coder_functions.find_by_status(target_status)
+ if coder_functions.delete_workspaces(workspaces):
+ return 'deleted all '+target_status+' workspaces'
+ else:
+ return 'failed to delete all '+target_status+' workspaces'
+
+# update old workspaces
+@app.route('/flask/update_old_workspaces', methods=['GET'])
+def update_all():
+ workspace_age = request.args.get('age')
+ names = coder_functions.update_old_workspaces(workspace_age)
+ return names
+
+@app.route('/flask/update_workspace', methods=['GET'])
+def test():
+ workspaces = coder_functions.list_workspaces('a')
+ workspace = coder_functions.search_workspaces(workspaces, unused_hours)
+
+ app.logger.debug('[Before] workspace_name= %s', workspace['name'])
+ workspace = coder_functions.update_workspace(workspace)
+ app.logger.debug('[After] workspace_name= %s', workspace['name'])
+
+ workspace_name = workspace['name']
+ workspace_status = workspace['latest_build']['status']
+ app.logger.debug('workspace_status= %s', workspace_status)
+ code_url = 'https://code--main--' + workspace_name + \
+ '--a.b.akamai-lab.com?folder=/home/coder'
+ origin_url = 'https://origin--main--' + workspace_name + '--a.b.akamai-lab.com'
+ app.logger.debug('code_url= '+code_url)
+ app.logger.debug('origin_url= '+origin_url)
+ return workspace_name
+
+
+
+@app.route('/flask/upgrade_workspaces', methods=['GET'])
+def upgrade_workspaces():
+ return 'under construction'
+
+@app.errorhandler(500)
+def internal_error(e):
+ return jsonify(error=str(e)), 500
diff --git a/build/python/coder_functions.py b/build/python/coder_functions.py
new file mode 100644
index 0000000..1ed8a25
--- /dev/null
+++ b/build/python/coder_functions.py
@@ -0,0 +1,354 @@
+from datetime import datetime, timedelta
+from flask import current_app
+import requests, time, subprocess, re
+
+# How can we update coder admin_token??? What is the maximum expiration days?
+# coder server --max-token-lifetime. Default 2540400 hours = 290 days
+# The maximum lifetime duration users can specify when creating an API token.
+# coder tokens create --lifetime. Default 720 hours = 30 days
+# Specify a duration for the lifetime of the token.
+# Create token API key - curl -X POST http://coder-server:8080/api/v2/users/{user}/keys/tokens
+# {
+# "lifetime": 0,
+# "scope": "all",
+# "token_name": "string"
+# }
+
+admin_token = 'gNmzL1TLeN-gXfs7q10uWPINqtlpt02Pj'
+template_id = 'a5577a86-e700-41be-997b-6001d78061d4'
+user_email = 'learn@akamai.com'
+user_pwd = 'Qodnwk=$s8'
+
+token_header = {'Coder-Session-Token': admin_token,
+ 'Accept': 'application/json'}
+token_param = {'email': user_email, 'password': user_pwd}
+
+# unused hours of workspace
+unused_hours = 2
+
+# workspace ttl ms
+ttl = 86400000 # 24 hours
+
+#########################################################################################
+# functions for end-users start
+# functions below are invoked when end-user click the link/button to get a workspace
+# list_workspaces(), search_workspaces(), create_workspace_name, update_workspace(), is_workspace_running(), is_agent_connected(), is_app_ready()
+##########################################################################################
+
+# this function get the all workspaces under the specified coder owner
+def list_workspaces(owner):
+ current_app.logger.debug('workspace_owner= %s', owner)
+ url = 'http://localhost:3000/api/v2/workspaces?owner=' + owner
+ response = requests.get(url, headers=token_header)
+ s_code = response.status_code
+ body = response.json()
+ current_app.logger.debug('list_workspace status code: '+str(s_code))
+ workspaces = []
+ if s_code == 200:
+ for workspace in body['workspaces']:
+ if workspace['owner_name'] == owner:
+ workspaces.append(workspace)
+ else:
+ current_app.logger.info('cannot get workspaces list')
+ workspaces = None
+
+ return workspaces
+
+# this function is main logic which finds an available workspace and return it
+def search_workspaces(workspaces, hours):
+ pattern = r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1-6}Z$'
+ selectedWorkspace = None
+ email = None
+ current_app.logger.debug('hours= '+str(hours))
+ # current_app.logger.debug('workspaces= '+str(workspaces))
+ for workspace in workspaces:
+ current_app.logger.debug('workspace= '+str(workspace))
+ is_selected = False
+ workspace_name = workspace['name']
+ current_app.logger.debug('workspace_name= '+workspace_name)
+
+ # 'last_used_at' looked useful. But it is updated too late. 5-10 minutes gap.
+ # so, we gave up using it.
+ # two code lines below remains just to show the debugging information.
+ last_used_at = workspace['last_used_at']
+ current_app.logger.debug('last_used_at= %s', last_used_at)
+
+ # Each time a workspace is assigned to an end-user, we rename the workspace to the current time.
+ # so, we use workspace name as 'last_assigned_at' variable
+ last_assigned_at = datetime.strptime(workspace['name'], '%Y%m%d%H%M%S')
+ current_app.logger.debug('last_assigned_at= %s', last_assigned_at)
+ currentDate = datetime.now()
+ current_app.logger.debug('now= %s', currentDate)
+ # we calculate how many hours has past since the last time the workspace was assigned.
+ current_app.logger.debug(currentDate - last_assigned_at)
+ difference = (currentDate - last_assigned_at).total_seconds() / 3600
+ current_app.logger.debug('hoursSinceLastAssigned= '+str(difference)+' hour(s)')
+
+ # if the workspace has been unassigned status longer than unused_hours, we select the workspace.
+ if difference > hours:
+ is_selected = True
+
+ # if workspace is selected, we update its name to the current timestamp
+ if is_selected:
+ selectedWorkspace =update_workspace(workspace)
+ current_app.logger.debug('selected workspace= '+str(selectedWorkspace))
+ break
+
+ if selectedWorkspace == None:
+ current_app.logger.info('cannot find an available workspace')
+ # we need to call create_workspaces() asynchronously
+
+ return selectedWorkspace
+
+# this function creates a new workspace name based on current time
+# How can I make sure this function is thread-safe, to avoid duplicate workspace_names???
+def create_workspace_name():
+# past = timedelta(hours=-unused_hours)
+# new_name = datetime.now() + past
+ new_name = str(datetime.now().strftime('%Y%m%d%H%M%S'))
+ current_app.logger.debug('new_name= %s'+new_name)
+ return new_name
+
+# this function updates a selected workspace with a new name created from create_workspace_name()
+def update_workspace(workspace):
+ selected_workspace = None
+ workspace_id = workspace['id']
+ workspace_name = workspace['name']
+ url = 'http://localhost:3000/api/v2/workspaces/' + workspace_id
+ new_name = create_workspace_name()
+ current_app.logger.debug('new_name= %s', new_name)
+ req_body = {"username": "a", "name": new_name}
+ current_app.logger.debug('update_workspace req_body= %s', req_body)
+
+ response = requests.patch(url, json=req_body, headers=token_header)
+ s_code = response.status_code
+ current_app.logger.debug('update_workspace status code: %s', str(s_code))
+ # body = response.json()
+ current_app.logger.debug('update_workspace response body: %s', response.content)
+
+ if s_code == 204:
+ current_app.logger.debug('workspace %s is updated to %s', workspace_name, new_name)
+ selected_workspace = requests.get('http://localhost:3000/api/v2/users/a/workspace/'+ new_name, headers=token_header).json()
+ current_app.logger.info('selected_workspace= %s', selected_workspace)
+ # following code lines double-check whether the workspace is successfully created or not.
+ # we commented them out. we might want to use them again for debugging later.
+ # time.sleep(0.5)
+ # response = requests.get(
+ # 'http://localhost:3000/api/v2/users/a/workspace/'+new_name, headers=token_header)
+ # s_code = response.status_code
+ # current_app.logger.debug(response.content)
+ #if s_code != 200:
+ # current_app.logger.info('cannot check update status of workspace ' + workspace_name)
+ # workspace = response.json()
+ else:
+ current_app.logger.info('cannot update workspace %s', workspace_name)
+ return selected_workspace
+
+# first, check workspace status
+def is_workspace_running(user_id):
+ workspace_status = 'unknown'
+ status_url = 'http://localhost:3000/api/v2/users/a/workspace/'+user_id
+ status_response = requests.get(status_url, headers=token_header)
+ current_app.logger.debug(user_id + ": is_workspace_running status code: " +
+ str(status_response.status_code))
+ status_json = status_response.json()
+
+ if status_json['latest_build']['status'] == 'running':
+ current_app.logger.info(user_id + ': workspace is running')
+ return True
+ else:
+ current_app.logger.info(user_id + ': workspace is NOT running')
+ return False
+
+# if workspace is not running, start it
+def start_workspace(user_id):
+ command = 'coder start a/'+user_id
+ result = subprocess.run([command], shell=True,
+ capture_output=True, text=True)
+ current_app.logger.debug(user_id + ': ' + result.stdout)
+ s_code = result.returncode
+ if s_code == 0:
+ return True
+ else:
+ return False
+
+# second, check the agent of the workspace
+# if it is not ready, wait
+def is_agent_connected(user_id):
+ agent_status = 'unknown'
+ count = 0
+ while agent_status != 'connected':
+ status_url = 'http://localhost:3000/api/v2/users/a/workspace/'+user_id
+ status_response = requests.get(status_url, headers=token_header)
+ current_app.logger.debug(
+ user_id + ": is_agent_connected status code: " + str(status_response.status_code))
+ status_json = status_response.json()
+ # app.logger.debug(user_id + ': '+str(status_json))
+ if len(status_json['latest_build']['resources']) > 1:
+ for resource in status_json['latest_build']['resources']:
+ if resource['type'] == 'docker_container':
+ for agent in resource['agents']:
+ if agent['name'] == 'main':
+ agent_status = agent['status']
+ current_app.logger.info(
+ user_id + ": agent status: " + agent_status)
+ if agent_status == 'timeout' or agent_status == 'connected':
+ break
+ count += 1
+ # will check coder is ready for the new url
+ time.sleep(1)
+
+ current_app.logger.debug(user_id + ": api call count: "+str(count))
+
+ if agent_status == 'connected':
+ return True
+ else:
+ return False
+
+# third, check the app running inside the workspace
+# there are 2 apps. coder-server and OWASP juice-shop
+def is_app_ready(url):
+ is_ready = False
+ while True:
+ response = requests.get(url)
+ s_code = response.status_code
+ current_app.logger.debug('%s status code= %s', url, s_code)
+ if s_code == 200:
+ is_ready = True
+ break
+ else:
+ time.sleep(1)
+ return is_ready
+
+########################################################################################
+# functions for end-users end
+########################################################################################
+
+
+
+##########################################################
+# The following functions are for administrator only!!! #
+#########################################################
+
+# create a workspace
+def create_workspace():
+ url = 'http://localhost:3000/api/v2/organizations/1a453979-dbd0-49f5-8d4c-5188c9027466/members/a/workspaces'
+ workspace_name = create_workspace_name()
+ param = {"name": workspace_name,
+ "template_id": "a5577a86-e700-41be-997b-6001d78061d4", "ttl_ms": ttl}
+
+ response = requests.post(url, json=param, headers=token_header)
+ s_code = response.status_code
+ current_app.logger.info(workspace_name +
+ ": create_workspace status: " + str(s_code))
+ body = response.json()
+ current_app.logger.debug(workspace_name + ': create_workspace body: ' + str(body))
+ if s_code == 201:
+ current_app.logger.info(workspace_name +" is created")
+ else:
+ current_app.logger.info(workspace_name +" cannot create a new workspace")
+ body = None
+ return body
+
+# create workspaces by the input numbers
+def create_workspaces(number_of_workspaces):
+ workspaces = []
+ for i in range(number_of_workspaces):
+ workspace = create_workspace()
+ workspaces.append(workspace)
+ time.sleep(number_of_workspaces)
+ if len(workspaces) > 0:
+ current_app.logger.info("workspaces are created")
+ else:
+ current_app.logger.info("cannot create any workspace")
+ workspaces = None
+ return workspaces
+
+# find workspaces by status. pending, starting, running, stopping, failed, canceling, canceled, deleting, deleted
+def find_by_status(target_status):
+ workspaces = list_workspaces('a')
+ new_workspaces = []
+ for workspace in workspaces:
+ status = workspace['latest_build']['status']
+ if status == target_status:
+ new_workspaces.append(workspace)
+
+ return new_workspaces
+
+# workspace deletion related functions start
+def delete_workspace(workspace):
+ workspace_name = workspace['name']
+ owner_name = workspace['owner_name']
+
+ command = 'coder delete ' + owner_name + '/' + workspace_name + ' -y'
+ result = subprocess.run([command], shell=True,
+ capture_output=True, text=True)
+ current_app.logger.debug(workspace_name + ': ' + result.stdout)
+ r_code = result.returncode
+ if r_code == 0:
+ return True
+ else:
+ return False
+
+def delete_workspaces(workspaces):
+ new_workspaces = []
+ for workspace in workspaces:
+ workspace_name = workspace['name']
+ if delete_workspace(workspace):
+ current_app.logger.info('deleted workspace %s', workspace_name)
+ new_workspaces.append(workspace)
+ else:
+ current_app.logger.info('failed to delete workspace %s', workspace_name)
+ if len(new_workspaces) == len(workspaces):
+ current_app.logger.info('deleted all workspaces')
+ return True
+ else:
+ return False
+
+
+# update workspace' name, if it is older than workspace_age (hours)
+def update_old_workspaces(workspace_age):
+ if isinstance(workspace_age, int) == False:
+ workspace_age = int(workspace_age)
+ names = {'old_names': [], 'new_names': []}
+ workspaces = list_workspaces('a')
+ regex = r'^\d{14}'
+ for workspace in workspaces:
+ need_update = False
+ old_name = workspace['name']
+
+ if re.match(regex, old_name):
+ difference = datetime.now() - datetime.strptime(old_name, '%Y%m%d%H%M%S')
+ current_app.logger.debug('difference= %s', difference)
+ if difference.total_seconds()/3600 > workspace_age:
+ need_update = True
+ else:
+ need_update = True
+
+ if need_update:
+ current_app.logger.debug('workspace= %s', workspace)
+ current_app.logger.debug('old_name= %s', old_name)
+ names['old_names'].append(old_name)
+ workspace = update_workspace(workspace)
+ current_app.logger.debug('new_name= %s', workspace['name'])
+ names['new_names'].append(workspace['name'])
+ # time.sleep(0.5)
+ return names
+
+# this function is now used.
+def create_token(user_id):
+ # The token expiry duration for browser sessions. Default 24 hours
+ # Sessions may last longer if they are actively making requests,
+ # but this functionality can be disabled via --disable-session-expiry-refresh.
+ token_url = 'http://localhost:3000/api/v2/users/a/keys'
+ token_response = requests.post(token_url, headers=token_header)
+ s_code = token_response.status_code
+ app.logger.debug('token response status code:' +
+ str(token_response.status_code))
+ if s_code == 201:
+ token_json = token_response.json()
+ app.logger.info(user_id + ': a new token is created')
+ app.logger.debug(user_id + ': '+token_json['key'])
+ return token_json['key']
+ else:
+ return ''
diff --git a/build/python/docebo_functions.py b/build/python/docebo_functions.py
new file mode 100644
index 0000000..5b68cbb
--- /dev/null
+++ b/build/python/docebo_functions.py
@@ -0,0 +1,23 @@
+import requests
+from flask import current_app
+
+def docebo_get_user(user_id, docebo_access_token):
+ if isinstance(user_id, str) == False:
+ user_id = str(user_id)
+ current_app.logger.debug('user_id= %s', user_id)
+
+ # View a User's info
+ # https://akamaisandbox.docebosaas.com/api-browser/#!/manage/User/User_manage_v1_user_user_id
+ url = 'https://akamaisandbox.docebosaas.com/manage/v1/user/'+user_id
+
+ headers = {'Authorization': 'Bearer '+str(docebo_access_token)}
+ response = requests.get(url, headers=headers)
+ s_code = response.status_code
+ body = response.json()
+ current_app.logger.debug('docebo_get_user response status code= '+str(s_code))
+ if s_code == 200:
+ docebo_user = body['data']['user_data']
+ current_app.logger.debug('docebo_get_user response body= '+str(docebo_user))
+ return docebo_user
+ else:
+ return None
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..0d307a2
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,47 @@
+version: "3.9"
+services:
+ coder:
+ # This MUST be stable for our documentation and
+ # other automations.
+ image: ghcr.io/coder/coder:${CODER_VERSION:-latest}
+ ports:
+ - "7080:7080"
+ environment:
+ CODER_PG_CONNECTION_URL: "postgresql://${POSTGRES_USER:-username}:${POSTGRES_PASSWORD:-password}@database/${POSTGRES_DB:-coder}?sslmode=disable"
+ CODER_HTTP_ADDRESS: "0.0.0.0:7080"
+ # You'll need to set CODER_ACCESS_URL to an IP or domain
+ # that workspaces can reach. This cannot be localhost
+ # or 127.0.0.1 for non-Docker templates!
+ CODER_ACCESS_URL: "${CODER_ACCESS_URL}"
+ # If the coder user does not have write permissions on
+ # the docker socket, you can uncomment the following
+ # lines and set the group ID to one that has write
+ # permissions on the docker socket.
+ #group_add:
+ # - "998" # docker group on host
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ depends_on:
+ database:
+ condition: service_healthy
+ database:
+ image: "postgres:14.2"
+ ports:
+ - "5432:5432"
+ environment:
+ POSTGRES_USER: ${POSTGRES_USER:-username} # The PostgreSQL user (useful to connect to the database)
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password} # The PostgreSQL password (useful to connect to the database)
+ POSTGRES_DB: ${POSTGRES_DB:-coder} # The PostgreSQL default database (automatically created at first launch)
+ volumes:
+ - coder_data:/var/lib/postgresql/data # Use "docker volume rm coder_coder_data" to reset Coder
+ healthcheck:
+ test:
+ [
+ "CMD-SHELL",
+ "pg_isready -U ${POSTGRES_USER:-username} -d ${POSTGRES_DB:-coder}",
+ ]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+volumes:
+ coder_data:
\ No newline at end of file