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('