diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a372f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +venv diff --git a/build/docker-compose.yaml b/build/docker-compose.yaml new file mode 100644 index 0000000..0d307a2 --- /dev/null +++ b/build/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 diff --git a/python/akamai_functions.py b/python/akamai_functions.py new file mode 100644 index 0000000..f15edb4 --- /dev/null +++ b/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/python/app.py b/python/app.py new file mode 100644 index 0000000..e83db5f --- /dev/null +++ b/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/python/coder_functions.py b/python/coder_functions.py new file mode 100644 index 0000000..1ed8a25 --- /dev/null +++ b/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/python/docebo_functions.py b/python/docebo_functions.py new file mode 100644 index 0000000..5b68cbb --- /dev/null +++ b/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