[
  {
    "path": ".gitignore",
    "content": "lambda/node_modules\nlambda/*.zip"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Ian Mckay\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# AWS Account Controller\n\n## Update March 2022: This is now largely deprecated due to the [CloseAccount](https://docs.aws.amazon.com/organizations/latest/APIReference/API_CloseAccount.html) method\n\n> Self-service creation and deletion of sandbox-style accounts\n\n<img width=\"680\" height=\"707\" src=\"https://github.com/iann0036/aws-account-controller/raw/master/assets/accountmanager.png\">\n\n> :exclamation: **PLEASE READ [THE CAVEATS](https://onecloudplease.com/blog/automating-aws-account-deletion) OF THIS SOLUTION BEFORE CONTINUING**\n\n## Prerequisites\n\nThe following is required before proceeding:\n\n* An AWS master account that has Organizations and SSO enabled\n* A credit card which will be used to apply payment information to terminated accounts (reloadable debit cards work also)\n* A [2Captcha](https://2captcha.com/) account that is sufficiently topped-up with credit ($10 would be more than enough)\n* A preferred master e-mail address to receive account correspondence to\n* A registered domain name or subdomain, which is publicly accessible\n* SES to have the master e-mail address be verified\n* SES to have either have the domain/subdomain also verified or have SES out of sandbox mode\n\n## Installation\n\n[![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=account-controller&templateURL=https://s3.amazonaws.com/ianmckay-us-east-1/accountcontroller/template.yml)\n\nClick the above link to deploy the stack to your environment. This stack creates:\n\n* Optionally, a Route 53 hosted zone (or provide your own by zone ID)\n* An MX record to SES inbound in the hosted zone\n* Node.js Lambda Function, used for all actions performed, with appropriate permissions\n* Log group for the Lambda Function, with a short term expiry\n* An S3 bucket for debugging screenshots, with a short term expiry\n* An S3 bucket for storing raw e-mail content, with a short term expiry\n* An SES Receipt Rule Set, which is automatically promoted to be default\n* An event rule that triggers Lambda execution when an organizations account is tagged for deletion (if enabled)\n* An API Gateway to service the SSO Account Manager application (if enabled)\n* An IAM user with a login profile, used to deploy a Connect instance and register the SSO application\n\nIf you prefer, you can also manually upsert the [template.yml](https://github.com/iann0036/aws-account-controller/blob/master/template.yml) stack from source.\n\nIf you chose to have the stack create a hosted zone for the account root e-mails instead of you bringing your own, you should ensure the nameservers of the new zone are associated with an accessible domains (automatic if the domain was created within Route 53).\n\nAlso make sure SES sending service limits are appropriate for the amount of e-mails you intend to receive.\n\nCurrently, the only tested region is `us-east-1`. The stack deploy time is approximately 8 minutes.\n\n#### Uninstallation\n\nTo remove this solution, ensure that both S3 buckets have their objects removed then delete the CloudFormation stack. The SES Receipt Rule Set will revert back to `default-rule-set`. An attempt will be made to terminate the Connect instance, however you should verify this occurs.\n\n## Usage\n\nIn order for you to easily build upon this system, the system makes heavy use of tags for system automation and configuration.\n\n### SSO Account Manager\n\nThe account manager (as seen at the top of this page) is a custom application that SSO users can access to create accounts or delete previously created accounts on-demand. It will be available to any user who is in the `AccountManagerUsers` SSO group. The application is accessible via the users SSO dashboard:\n\n[![SSO Dashboard](assets/sso.png)](assets/sso.png)\n\nThe application will ensure only accounts owned by the creator are shown, unless the creator explicitly shares the account with other users, in which case it will be shared with all users who are also in the `AccountManagerUsers` SSO group. Accounts which are created can optionally require a monthly budget to be set, which if exceeded will automatically trigger a deletion of the account (the maximum budget is an option during installation). Note that actual account spend may exceed the budget as pricing metrics can be delayed up to 6 hours or more for some services.\n\nDuring installation, if you select `true` for the `Deny Subscription Calls` parameter, a number of calls will be denied to created accounts via an SCP such as calls to create reserved instances, register domain names or apply S3 object locks.\n\nYou can also elect not to include the SSO functionality by selecting `false` during installation for the `Enable Account Creation Functionality` parameter. You do not require SSO to be enabled within the account if you select this option.\n\n### E-mail Forwarding\n\nE-mails that are targetting the addresses of the root account will be forwarded by default to the master e-mail address.\n\n[![Email Forwarding](assets/email.png)](assets/email.png)\n\nYou can specify a different destination per account by placing a tag with the key `AccountEmailForwardingAddress` on the account in Organizations. This is set to the SSO user automatically if the account was created with the SSO Account Manager application and the `Send Root E-mails to User` parameter was set to `true` during installation.\n\nYou can also override the format of the subject line for forwarded e-mails. During installation, you can change the subject line to any string with the following variables available for substitution:\n\n* {from} - The From address of the original e-mail\n* {to} - The To address of the original e-mail\n* {subject} - The subject of the original e-mail\n* {accountid} - The ID of the account\n* {accountname} - The name of the account\n* {accountemail} - The root email address of the account\n\n### Account Deletion (Manual Method)\n\nIn order to elect to delete an account without the use of the SSO Account Manager, simply tag an account within the Organizations console with the following (case not sensitive):\n\n*Tag Key:* **Delete**\n\n*Tag Value:* **true**\n\n[![Email Forwarding](assets/tags.png)](assets/tags.png)\n\nOnce tagged, a process will perform the following actions on your behalf:\n\n* Trigger a password reset for the root account\n* Reset the password to the automatically generated master password\n* Add payment information to the account\n* Perform a phone verification of the account\n* Close the account\n* Remove (or schedule removal of) the account from Organizations\n\nThe above process takes approximately 4 minutes.\n\nIf the account more than 7 days old, the process completely remove the account from Organizations. If the account is less than 7 days old, a tag with the key `AccountDeletionTime` will be set with the timestamp the account was deleted at and another tag with the key `ScheduledRemovalTime` will be set with the timestamp the account will be removed from Organizations.\n\nYou can also elect not to include the deletion functionality by selecting `false` during installation to the `Enable Account Deletion Functionality` parameter.\n\n### Other Features / Options\n\nThere are some other features and options that may be specified during installation. These include:\n\n* `Unsubscribe Marketing E-mails` - if set to `true`, newly created accounts will be unsubscribed from all AWS marketing material\n* `SSO Account Manager Application Name` - sets a custom name for the SSO Account Manager\n* `Automation IAM User Username` - sets a custom username for the IAM user used to perform Connect and/or SSO functions\n* `Maximum Monthly Spend Per Account` - enforces a custom upper limit on the monthly budget new accounts can request, or disables budgets completely\n* `Deny Subscription Calls` - if set to `true`, a service control policy which restricts the use of subscription-based calls, like reserved instances, will be applied to new accounts\n* `Control Tower Mode` - if set to `true`, accounts will be created with the Control Tower Account Factory, rather than via Organizations directly\n\n## Architecture\n\n[![Architecture Diagram](assets/arch.svg)](assets/arch.svg)\n\n## Disclaimer\n\nPer the original [post](https://onecloudplease.com/blog/automating-aws-account-deletion), I highly recommend you do not use this in an organization that has production workloads associated with it. It is intended for [developer accounts](https://youtu.be/Fxkbz0OwPKk?t=475) only.\n"
  },
  {
    "path": "assets/arch.drawio.xml",
    "content": "<mxfile host=\"app.diagrams.net\" modified=\"2020-04-10T11:34:12.133Z\" agent=\"5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36\" etag=\"dj3M7wHyo0wU1roODCVc\" version=\"12.9.10\" type=\"device\"><diagram id=\"VmFleLBM2T6B-wKIucCD\" name=\"Page-1\">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</diagram></mxfile>"
  },
  {
    "path": "lambda/index.js",
    "content": "/*\n\nCreateConnect Event:\n\n{\n  \"properties\": {\n    \"Domain\": \"9030bff7\"\n  }\n}\n\nResetEmail Event:\n\n{\n  \"email\": \"example7@yourdomain.com\"\n}\n\n*/\n\nconst chromium = require('chrome-aws-lambda');\nconst puppeteer = require('puppeteer-core');\nconst AWS = require('aws-sdk');\nconst fs = require('fs');\nconst url = require('url');\nvar rp = require('request-promise');\nvar winston = require('winston');\nvar InternetMessage = require(\"internet-message\");\nvar saml2 = require('saml2-js');\nvar dateFormat = require('dateformat');\n\nvar LOG = winston.createLogger({\n    level: process.env.LOG_LEVEL.toLowerCase(),\n    transports: [\n        new winston.transports.Console()\n    ]\n});\n\nvar s3 = new AWS.S3();\nvar ssm = new AWS.SSM();\nvar rekognition = new AWS.Rekognition();\nvar organizations = new AWS.Organizations();\nvar ses = new AWS.SES();\nvar eventbridge = new AWS.EventBridge();\nvar secretsmanager = new AWS.SecretsManager();\nvar sts = new AWS.STS();\nvar servicecatalog = new AWS.ServiceCatalog();\n\nconst CAPTCHA_KEY = process.env.CAPTCHA_KEY;\nconst MASTER_EMAIL = process.env.MASTER_EMAIL;\nconst ACCOUNTID = process.env.ACCOUNTID;\n\nconst sendcfnresponse = async (event, context, responseStatus, responseData, physicalResourceId, noEcho) => {\n    var responseBody = JSON.stringify({\n        Status: responseStatus,\n        Reason: \"See the details in CloudWatch Log Stream: \" + context.logStreamName,\n        PhysicalResourceId: physicalResourceId || event.LogicalResourceId,\n        StackId: event.StackId,\n        RequestId: event.RequestId,\n        LogicalResourceId: event.LogicalResourceId,\n        NoEcho: noEcho || false,\n        Data: responseData\n    });\n \n    LOG.debug(\"Response body:\\n\", responseBody);\n \n    var https = require(\"https\");\n    var url = require(\"url\");\n \n    var parsedUrl = url.parse(event.ResponseURL);\n    var options = {\n        hostname: parsedUrl.hostname,\n        port: 443,\n        path: parsedUrl.path,\n        method: \"PUT\",\n        headers: {\n            \"content-type\": \"\",\n            \"content-length\": responseBody.length\n        }\n    };\n \n    await new Promise((resolve, reject) => {\n        var request = https.request(options, function(response) {\n            LOG.debug(\"Status code: \" + response.statusCode);\n            LOG.debug(\"Status message: \" + response.statusMessage);\n            resolve();\n        });\n     \n        request.on(\"error\", function(error) {\n            LOG.warn(\"send(..) failed executing https.request(..): \" + error);\n            reject();\n        });\n     \n        request.write(responseBody);\n        request.end();\n    });\n}\n\n\n\nconst solveCaptcha = async (page, url) => {\n    var captchaResult = \"\";\n\n    if (process.env.CAPTCHA_STRATEGY == \"Rekognition\") {\n        captchaResult = await solveCaptchaRekog(page, url);\n    } else {\n        captchaResult = await solveCaptcha2captcha(page, url);\n    }\n\n    return captchaResult;\n};\n\nconst solveCaptchaRekog = async (page, url) => {\n    var imgbody = await rp({ uri: url, method: 'GET', encoding: null }).then(res => {\n        return res;\n    });\n\n    var code = null;\n\n    let data = await rekognition.detectText({\n        Image: {\n            Bytes: Buffer.from(imgbody)\n        }\n    }).promise();\n\n    if (data) {\n        data.TextDetections.forEach(textDetection => {\n            var text = textDetection.DetectedText.replace(/\\ /g, \"\");\n            if (text.length == 6) {\n                code = text;\n            }\n        });\n    }\n\n    LOG.debug(code);\n\n    if (!code) {\n        await page.click('.refresh');\n        await page.waitFor(5000);\n    }\n\n    return code;\n}\n\nconst solveCaptcha2captcha = async (page, url) => {\n    var imgbody = await rp({ uri: url, method: 'GET', encoding: null }).then(res => {\n        return res;\n    });\n\n    var captcharef = await rp({ uri: 'http://2captcha.com/in.php', method: 'POST', body: JSON.stringify({\n        'key': CAPTCHA_KEY,\n        'method': 'base64',\n        'body': \"data:image/jpeg;base64,\" + Buffer.from(imgbody).toString('base64')\n    })}).then(res => {\n        LOG.debug(res);\n        return res.split(\"|\").pop();\n    });\n\n    var captcharesult = '';\n    var i = 0;\n    while (!captcharesult.startsWith(\"OK\") && i < 20) {\n        await new Promise(resolve => { setTimeout(resolve, 5000); });\n\n        var captcharesult = await rp({ uri: 'http://2captcha.com/res.php?key=' + CAPTCHA_KEY + '&action=get&id=' + captcharef, method: 'GET' }).then(res => {\n            LOG.debug(res);\n            return res;\n        });\n\n        i++;\n    }\n\n    return captcharesult.split(\"|\").pop();\n}\n\nconst uploadResult = async (url, data) => {\n    await rp({ uri: url, method: 'PUT', body: JSON.stringify(data) });\n}\n\nconst debugScreenshot = async (page) => {\n    if (LOG.level == \"debug\") {\n        let filename = Date.now().toString() + \".png\";\n\n        await page.screenshot({ path: '/tmp/' + filename });\n\n        await new Promise(function (resolve, reject) {\n            fs.readFile('/tmp/' + filename, (err, data) => {\n                if (err) LOG.error(err);\n\n                var base64data = Buffer.from(data);\n\n                var params = {\n                    Bucket: process.env.DEBUG_BUCKET,\n                    Key: filename,\n                    Body: base64data\n                };\n\n                s3.upload(params, (err, data) => {\n                    if (err) LOG.error(`Upload Error ${err}`);\n                    LOG.debug('Debug screenshot upload completed - ' + filename);\n                    resolve();\n                });\n            });\n        });\n    }\n};\n\nasync function retryWrapper(client, method, params) {\n    return new Promise((resolve, reject) => {\n        client[method](params).promise().then(data => {\n            resolve(data);\n        }).catch(err => {\n            if (err.code == \"TooManyRequestsException\") {\n                LOG.debug(\"Got TooManyRequestsException, sleeping 5s\");\n                setTimeout(() => {\n                    retryWrapper(client, method, params).then(data => {\n                        resolve(data);\n                    }).catch(err => {\n                        reject(err);\n                    });\n                }, 5000); // 5s\n            } else if (err.code == \"OptInRequired\") {\n                LOG.debug(\"Got OptInRequired, sleeping 20s\");\n                setTimeout(() => {\n                    retryWrapper(client, method, params).then(data => {\n                        resolve(data);\n                    }).catch(err => {\n                        reject(err);\n                    });\n                }, 20000); // 20s\n            } else {\n                reject(err);\n            }\n        });\n    });\n}\n\nasync function login(page) {\n    let secretsmanagerresponse = await secretsmanager.getSecretValue({\n        SecretId: process.env.SECRET_ARN\n    }).promise();\n\n    let secretdata = JSON.parse(secretsmanagerresponse.SecretString);\n\n    var passwordstr = secretdata.password;\n\n    await page.goto('https://' + process.env.ACCOUNTID + '.signin.aws.amazon.com/console', {\n        timeout: 0,\n        waitUntil: ['domcontentloaded']\n    });\n    await debugScreenshot(page);\n\n    await page.waitFor(2000);\n\n    let username = await page.$('#username');\n    await username.press('Backspace');\n    await username.type(secretdata.username, { delay: 100 });\n\n    let password = await page.$('#password');\n    await password.press('Backspace');\n    await password.type(passwordstr, { delay: 100 });\n\n    await page.click('#signin_button');\n\n    await debugScreenshot(page);\n\n    await page.waitFor(5000);\n}\n\nasync function createssoapp(page, properties) {\n    await page.goto('https://console.aws.amazon.com/singlesignon/home?region=' + process.env.AWS_REGION + '#/applications/add', {\n        timeout: 0,\n        waitUntil: ['domcontentloaded']\n    });\n    await page.waitFor(5000);\n\n    await debugScreenshot(page);\n\n    const cookies = await page.cookies();\n\n    let cookie = \"\";\n    cookies.forEach(cookieitem => {\n        cookie += cookieitem['name'] + \"=\" + cookieitem['value'] + \"; \";\n    });\n    cookie = cookie.substr(0, cookie.length - 2);\n\n    let csrftoken = await page.$eval('head > meta[name=\"awsc-csrf-token\"]', element => element.content);\n\n    let accountmanagergroupresult = await rp({\n        uri: 'https://console.aws.amazon.com/singlesignon/api/userpool',\n        method: 'POST',\n        body: JSON.stringify({\n            \"method\": \"POST\",\n            \"path\": \"/userpool/\",\n            \"headers\": {\n                \"Content-Type\": \"application/json; charset=UTF-8\",\n                \"Content-Encoding\": \"amz-1.0\",\n                \"X-Amz-Target\": \"com.amazonaws.swbup.service.SWBUPService.SearchGroups\",\n                \"X-Amz-Date\": dateFormat(new Date(), \"GMT:ddd, dd mmm yyyy HH:MM:ss\") + \" GMT\",\n                \"Accept\": \"application/json, text/javascript, */*\"\n            },\n            \"region\": \"us-east-1\",\n            \"operation\": \"SearchGroups\",\n            \"contentString\": JSON.stringify({\n                \"SearchString\": \"AccountManagerUsers*\",\n                \"SearchAttributes\": [\n                    \"GroupName\"\n                ],\n                \"MaxResults\": 100,\n                \"NextToken\": null\n            })\n        }),\n        headers: {\n            'accept': 'application/json, text/plain, */*',\n            'content-type': 'application/json',\n            'x-csrf-token': csrftoken,\n            'cookie': cookie\n        }\n    });\n\n    let groupid = null;\n    let accountmanagergroups = JSON.parse(accountmanagergroupresult).Groups;\n    if (accountmanagergroups.length == 0) {\n        let creategroupresult = await rp({\n            uri: 'https://console.aws.amazon.com/singlesignon/api/userpool',\n            method: 'POST',\n            body: JSON.stringify({\n                \"method\": \"POST\",\n                \"path\": \"/userpool/\",\n                \"headers\": {\n                    \"Content-Type\": \"application/json; charset=UTF-8\",\n                    \"Content-Encoding\": \"amz-1.0\",\n                    \"X-Amz-Target\": \"com.amazonaws.swbup.service.SWBUPService.CreateGroup\",\n                    \"X-Amz-Date\": dateFormat(new Date(), \"GMT:ddd, dd mmm yyyy HH:MM:ss\") + \" GMT\",\n                    \"Accept\": \"application/json, text/javascript, */*\"\n                },\n                \"region\": \"us-east-1\",\n                \"operation\": \"CreateGroup\",\n                \"contentString\": JSON.stringify({\n                    \"GroupName\": \"AccountManagerUsers\"\n                })\n            }),\n            headers: {\n                'accept': 'application/json, text/plain, */*',\n                'content-type': 'application/json',\n                'x-csrf-token': csrftoken,\n                'cookie': cookie\n            }\n        });\n\n        groupid = JSON.parse(creategroupresult).Group.GroupId;\n    } else {\n        groupid = accountmanagergroups[0].GroupId;\n    }\n    \n    await page.click('.add-custom-application-text');\n\n    await page.waitFor(5000);\n\n    await debugScreenshot(page);\n\n    let signinurlel = await page.$('awsui-control-group[label=\"AWS SSO sign-in URL\"] > div > div > div > span > div > input');\n    properties['SignInURL'] = await page.evaluate((obj) => {\n        return obj.value;\n    }, signinurlel);\n\n    LOG.debug(\"Signin URL: \" + properties['SignInURL']);\n\n    let signouturlel = await page.$('awsui-control-group[label=\"AWS SSO sign-out URL\"] > div > div > div > span > div > input');\n    properties['SignOutURL'] = await page.evaluate((obj) => {\n        return obj.value;\n    }, signouturlel);\n\n    LOG.debug(\"Signout URL: \" + properties['SignOutURL']);\n\n    await page._client.send('Page.setDownloadBehavior', {behavior: 'allow', downloadPath: '/tmp/'});\n    await page.click('awsui-button[click=\"peregrineMetadata.downloadCertificate()\"] > button');\n\n    let appdisplayname = await page.$('awsui-textfield[ng-model=\"configureApplication.displayName\"] > input');\n    await page.evaluate((obj) => {\n        return obj.value = \"\";\n    }, appdisplayname);\n    await appdisplayname.press('Backspace');\n    await appdisplayname.type(properties.SSOManagerAppName, { delay: 100 });\n\n    let appdescription = await page.$('awsui-textarea[ng-model=\"configureApplication.description\"] > textarea');\n    await page.evaluate((obj) => {\n        return obj.value = \"\";\n    }, appdescription);\n    await appdescription.press('Backspace');\n    await appdescription.type(\"AWS Accounts Manager\", { delay: 100 });\n\n    await page.click('awsui-button[click=\"configureApplication.toggleServiceProviderConfiguration()\"]'); // manual metadata values\n\n    await page.waitFor(200);\n\n    let acsurl = await page.$('awsui-textfield[ng-model=\"configureApplication.loginURL\"] > input');\n    await acsurl.press('Backspace');\n    await acsurl.type(properties['APIGatewayEndpoint'] + \"/\", { delay: 100 });\n    \n    let samlaudience = await page.$('awsui-textfield[ng-model=\"configureApplication.samlAudience\"] > input');\n    await samlaudience.press('Backspace');\n    await samlaudience.type(\"https://\" + process.env.DOMAIN_NAME + \"/metadata.xml\", { delay: 100 });\n\n    await debugScreenshot(page);\n\n    await page.click('awsui-button[click=\"configureApplication.saveChanges()\"]'); // save\n    \n    await page.waitFor(5000);\n\n    fs.readdirSync('/tmp/').forEach(file => {\n        if (file.endsWith(\"certificate.pem\")) {\n            properties['Certificate'] = fs.readFileSync('/tmp/' + file, 'utf8');\n            fs.unlinkSync('/tmp/' + file);\n        }\n    });\n\n    await debugScreenshot(page);\n\n    await ssm.putParameter({\n        Name: process.env.SSO_SSM_PARAMETER,\n        Type: \"String\",\n        Value: JSON.stringify(properties),\n        Overwrite: true\n    }).promise();\n\n    // map attributes\n    LOG.debug(\"Started mapping attributes\");\n\n    await debugScreenshot(page);\n\n    let paneltabs = await page.$$('.awsui-tabs-container > li');\n    await paneltabs[1].click();\n\n    await page.waitFor(500);\n\n    await debugScreenshot(page);\n\n    await page.click('awsui-select[ng-model=\"item.schemaProperty.nameIdFormat\"]');\n    await page.waitFor(200);\n    await page.click('li[data-value=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\"]');\n\n    let attrmappings = {\n        'Subject': '${user:AD_GUID}', // required\n        'name': '${user:name}',\n        'guid': '${user:AD_GUID}',\n        'email': '${user:email}'\n    }\n\n    for (const attr in attrmappings) {\n        if (attr != \"Subject\") {\n            await page.click('.add-attribute');\n\n            let samlattrnames = await page.$$('awsui-textfield[ng-model=\"item.key\"] > input');\n            let samlattrname = samlattrnames.pop();\n            await samlattrname.press('Backspace');\n            await samlattrname.type(attr, { delay: 100 });\n        }\n\n        let samlattrvals = await page.$$('awsui-textfield[ng-model=\"item.property.source[0]\"] > input'); // .ng-invalid-saml-attribute > input\n        let samlattrval = samlattrvals.pop();\n        await samlattrval.press('Backspace');\n        await samlattrval.type(attrmappings[attr], { delay: 100 });\n\n        await page.waitFor(200);\n    }\n\n    await debugScreenshot(page);\n\n    await page.click('awsui-button[click=\"samlSection.saveChanges()\"]'); // Save changes\n\n    LOG.debug(\"Finished mapping attributes, mapping app to group\");\n\n    await page.waitFor(5000);\n\n    await paneltabs[2].click(); // users and group mappings\n    await page.waitFor(2000);\n\n    await debugScreenshot(page);\n\n    await page.click('.assign-users-button');\n    await page.waitFor(5000);\n\n    await debugScreenshot(page);\n\n    let paneltabs2 = await page.$$('.awsui-tabs-container > li');\n    await paneltabs2.pop().click(); // last tab\n    await page.waitFor(5000);\n\n    await debugScreenshot(page);\n\n    let groupsearch = await page.$('awsui-textfield[ng-model=\"table.controlValues.search\"] > input');\n    await groupsearch.press('Backspace');\n    await groupsearch.type('AccountManagerUsers', { delay: 100 });\n    await page.waitFor(5000);\n\n    await debugScreenshot(page);\n\n    await page.click('div.group-name > div.selection > div.checkbox > awsui-checkbox');\n    await page.waitFor(200);\n    \n    await page.click('.assign'); // assign users button\n\n    await page.waitFor(5000);\n\n    await debugScreenshot(page);\n\n    return properties;\n}\n\nasync function deletessoapp(page, properties) {\n    await page.goto('https://console.aws.amazon.com/singlesignon/home?region=' + process.env.AWS_REGION + '#/applications', {\n        timeout: 0,\n        waitUntil: ['domcontentloaded']\n    });\n    await page.waitFor(5000);\n\n    let apptooltip = await page.$$('truncate[tooltip=\"' + properties.SSOManagerAppName + '\"]');\n    if (apptooltip.length == 1) {\n        await page.evaluate((obj) => {\n            return obj.parentNode.parentNode.parentNode.firstElementChild.click();\n        }, apptooltip[0]);\n        await page.waitFor(200);\n\n        await page.click('awsui-button-dropdown[text=\"Actions\"]');\n        await page.waitFor(200);\n\n        let dropdownitems = await page.$$('.awsui-button-dropdown-item-content');\n        await dropdownitems.forEach(async (item) => {\n            await page.evaluate((obj) => {\n                if (obj.innerText.trim() == \"Remove\") {\n                    obj.click();\n                }\n            }, item);\n        });\n        await page.waitFor(1000);\n\n        await page.click('.modal-confirm');\n        await page.waitFor(6000);\n\n        await debugScreenshot(page);\n    } else {\n        LOG.warn(\"Multiple SSO applications of the same name found, skipping\");\n    }\n}\n\nasync function createinstance(page, properties) {\n    await page.goto('https://' + process.env.AWS_REGION + '.console.aws.amazon.com/connect/onboarding', {\n        timeout: 0,\n        waitUntil: ['domcontentloaded']\n    });\n    await page.waitFor(5000);\n\n    let directory = await page.$('input[ng-model=\"ad.directoryAlias\"]');\n    await directory.press('Backspace');\n    await directory.type(properties.Domain, { delay: 100 });\n\n    page.focus('button.awsui-button-variant-primary');\n    await page.click('button.awsui-button-variant-primary');\n\n    await page.waitForSelector('label.vertical-padding.option-label');\n    await page.waitFor(200);\n    let skipradio = await page.$$('label.vertical-padding.option-label');\n    skipradio.pop().click();\n\n    await page.waitFor(200);\n\n    await page.click('button[type=\"submit\"].awsui-button-variant-primary');\n\n    await page.waitFor(200);\n\n    await page.click('button[type=\"submit\"].awsui-button-variant-primary');\n\n    await page.waitFor(200);\n\n    await page.click('button[type=\"submit\"].awsui-button-variant-primary');\n\n    await page.waitFor(200);\n\n    await page.click('button[type=\"submit\"].awsui-button-variant-primary');\n\n    await page.waitFor(200);\n\n    await page.click('button[type=\"submit\"].awsui-button-variant-primary');\n\n    await page.waitForSelector('.onboarding-success-message', {timeout: 180000});\n\n    await debugScreenshot(page);\n\n    await page.waitFor(3000);\n}\n\nasync function open(page, properties) {\n    await page.goto('https://' + process.env.AWS_REGION + '.console.aws.amazon.com/connect/home', {\n        timeout: 0,\n        waitUntil: ['domcontentloaded']\n    });\n    await page.waitFor(8000);\n\n    await debugScreenshot(page);\n\n    await page.waitFor(3000);\n\n    await page.click('table > tbody > tr > td:nth-child(1) > div > a');\n\n    await page.waitFor(5000);\n\n    let loginbutton = await page.$('.emergency-access a');\n    let loginlink = await page.evaluate((obj) => {\n        return obj.getAttribute('href');\n    }, loginbutton);\n\n    await page.goto('https://' + process.env.AWS_REGION + '.console.aws.amazon.com' + loginlink, {\n        timeout: 0,\n        waitUntil: ['domcontentloaded']\n    });\n\n    await page.waitFor(8000);\n\n    await debugScreenshot(page);\n}\n\nasync function deleteinstance(page, properties) {\n    await page.goto('https://' + process.env.AWS_REGION + '.console.aws.amazon.com/connect/home', {\n        timeout: 0,\n        waitUntil: ['domcontentloaded']\n    });\n    await page.waitFor(8000);\n\n    await debugScreenshot(page);\n\n    await page.waitFor(3000);\n\n    let checkbox = await page.$$('awsui-checkbox > label > input');\n    await checkbox[0].click();\n    await page.waitFor(200);\n\n    await debugScreenshot(page);\n    LOG.debug(\"Clicked checkbox\");\n\n    let removebutton = await page.$$('button[type=\"submit\"]');\n    LOG.debug(removebutton.length);\n    await removebutton[1].click();\n    LOG.debug(\"Clicked remove\");\n    await page.waitFor(200);\n\n    let directory = await page.$('.awsui-textfield-type-text');\n    await directory.press('Backspace');\n    await directory.type(properties.Domain, { delay: 100 });\n    await page.waitFor(200);\n\n    await page.click('awsui-button[click=\"confirmDeleteOrg()\"] > button');\n    await page.waitFor(5000);\n\n    await debugScreenshot(page);\n}\n\nasync function claimnumber(page, properties) {\n    let host = 'https://' + new url.URL(await page.url()).host;\n\n    LOG.debug(host + '/connect/numbers/claim');\n\n    await page.goto(host + '/connect/numbers/claim', {\n        timeout: 0,\n        waitUntil: ['domcontentloaded']\n    });\n    await page.waitFor(5000);\n\n    await debugScreenshot(page);\n\n    await page.waitFor(3000);\n\n    await page.click('li[heading=\"DID (Direct Inward Dialing)\"] > a');\n\n    await page.waitFor(200);\n\n    await page.click('div.active > span > div.country-code-real-input');\n\n    await page.waitFor(200);\n\n    await page.click('div.active > span.country-code-input.ng-scope > ul > li > .us-flag'); // USA\n\n    await page.waitFor(5000);\n\n    await page.click('div.active > awsui-radio-group > div > span > div:nth-child(1) > awsui-radio-button > label.awsui-radio-button-wrapper-label > div'); // Phone number selection\n\n    let phonenumber = await page.$('div.active > awsui-radio-group > div > span > div:nth-child(1) > awsui-radio-button > label.awsui-radio-button-checked.awsui-radio-button-label > div > span > div');\n    let phonenumbertext = await page.evaluate(el => el.textContent, phonenumber);\n\n    await page.waitFor(200);\n\n    await debugScreenshot(page);\n\n    let disclaimerlink = await page.$('div.tab-pane.ng-scope.active > div.alert.alert-warning.ng-scope > a');\n    if (disclaimerlink !== null) {\n        disclaimerlink.click();\n    }\n\n    await page.waitFor(200);\n\n    await debugScreenshot(page);\n\n    await page.click('#s2id_select-width > a');\n    \n    await page.waitFor(2000);\n\n    await debugScreenshot(page);\n\n    let s2input = await page.$('#select2-drop > div > input');\n    await s2input.press('Backspace');\n    await s2input.type(\"myFlow\", { delay: 100 });\n    await page.waitFor(2000);\n    await s2input.press('Enter');\n    await page.waitFor(1000);\n\n    await debugScreenshot(page);\n\n    await page.click('awsui-button[text=\"Save\"] > button');\n    await page.waitFor(5000);\n\n    await debugScreenshot(page);\n\n    return {\n        'PhoneNumber': phonenumbertext\n    };\n}\n\nasync function uploadprompts(page, properties) {\n    let host = 'https://' + new url.URL(await page.url()).host;\n\n    let ret = {};\n    \n    let prompt_filenames = [\n        'a-10-second-silence.wav',\n        '9.wav',\n        '8.wav',\n        '7.wav',\n        '6.wav',\n        '5.wav',\n        '4.wav',\n        '3.wav',\n        '2.wav',\n        '1.wav',\n        '0.wav'\n    ];\n    \n    for (var pid in prompt_filenames) {\n        let filename = prompt_filenames[pid];\n\n        do {\n            await page.goto(host + \"/connect/prompts/create\", {\n                timeout: 0,\n                waitUntil: ['domcontentloaded']\n            });\n            await page.waitFor(5000);\n            LOG.info(\"Checking for correct load\");\n            LOG.debug(host + \"/connect/prompts/create\");\n        } while (await page.$('#uploadFileBox') === null);\n\n        await debugScreenshot(page);\n\n        const fileInput = await page.$('#uploadFileBox');\n        await fileInput.uploadFile(process.env.LAMBDA_TASK_ROOT + '/prompts/' + filename);\n\n        await page.waitFor(1000);\n\n        let input1 = await page.$('#name');\n        await input1.press('Backspace');\n        await input1.type(filename, { delay: 100 });\n\n        await debugScreenshot(page);\n\n        await page.waitFor(1000);\n\n        await page.click('#lily-save-resource-button');\n\n        await page.waitFor(8000);\n\n        await debugScreenshot(page);\n        \n        await page.$('#collapsePrompt0 > div > div:nth-child(2) > table > tbody > tr > td');\n        let promptid = await page.$eval('#collapsePrompt0 > div > div:nth-child(2) > table > tbody > tr > td', el => el.textContent);\n        LOG.debug(\"PROMPT ID:\");\n        LOG.debug(promptid);\n        ret[filename] = promptid;\n    };\n\n    await debugScreenshot(page);\n\n    return ret;\n}\n\nasync function createflow(page, properties, prompts) {\n    let host = 'https://' + new url.URL(await page.url()).host;\n    \n    do {\n        await page.goto(host + \"/connect/contact-flows/create?type=contactFlow\", {\n            timeout: 0,\n            waitUntil: ['domcontentloaded']\n        });\n        await page.waitFor(5000);\n        LOG.info(\"Checking for correct load\");\n        LOG.debug(host + \"/connect/contact-flows/create?type=contactFlow\");\n    } while (await page.$('#angularContainer') === null);\n\n    await debugScreenshot(page);\n\n    await page.click('#can-edit-contact-flow > div > awsui-button > button');\n\n    await page.waitFor(200);\n\n    await debugScreenshot(page);\n\n    await page.click('#cf-dropdown a[ng-click=\"verifyImport()\"]');\n\n    await page.waitFor(500);\n\n    await page.setBypassCSP(true);\n\n    await debugScreenshot(page);\n\n    let flow = `{\n    \"modules\": [\n        {\n            \"id\": \"a238d7ff-9df4-481b-bcf5-e472c3a51abf\",\n            \"type\": \"PlayPrompt\",\n            \"branches\": [\n                {\n                    \"condition\": \"Success\",\n                    \"transition\": \"39ca9b44-c416-45eb-b2c0-591956bd2fe9\"\n                }\n            ],\n            \"parameters\": [\n                {\n                    \"name\": \"AudioPrompt\",\n                    \"value\": \"prompt2\",\n                    \"namespace\": \"External\",\n                    \"resourceName\": null\n                }\n            ],\n            \"metadata\": {\n                \"position\": {\n                    \"x\": 700,\n                    \"y\": 16\n                },\n                \"useDynamic\": true\n            }\n        },\n        {\n            \"id\": \"1f4d3616-77cc-4cef-8881-949c531e13ce\",\n            \"type\": \"PlayPrompt\",\n            \"branches\": [\n                {\n                    \"condition\": \"Success\",\n                    \"transition\": \"a238d7ff-9df4-481b-bcf5-e472c3a51abf\"\n                }\n            ],\n            \"parameters\": [\n                {\n                    \"name\": \"AudioPrompt\",\n                    \"value\": \"prompt1\",\n                    \"namespace\": \"External\",\n                    \"resourceName\": null\n                }\n            ],\n            \"metadata\": {\n                \"position\": {\n                    \"x\": 456,\n                    \"y\": 19\n                },\n                \"useDynamic\": true\n            }\n        },\n        {\n            \"id\": \"ad3b6726-dfed-40fe-b4c7-95a9751fc4a7\",\n            \"type\": \"InvokeExternalResource\",\n            \"branches\": [\n                {\n                    \"condition\": \"Success\",\n                    \"transition\": \"1f4d3616-77cc-4cef-8881-949c531e13ce\"\n                },\n                {\n                    \"condition\": \"Error\",\n                    \"transition\": \"f5205242-eeb0-4b71-bb47-f8c2adf848fa\"\n                }\n            ],\n            \"parameters\": [\n                {\n                    \"name\": \"FunctionArn\",\n                    \"value\": \"arn:aws:lambda:us-east-1:${ACCOUNTID}:function:AccountAutomator\",\n                    \"namespace\": null\n                },\n                {\n                    \"name\": \"TimeLimit\",\n                    \"value\": \"8\"\n                }\n            ],\n            \"metadata\": {\n                \"position\": {\n                    \"x\": 191,\n                    \"y\": 15\n                },\n                \"dynamicMetadata\": {},\n                \"useDynamic\": false\n            },\n            \"target\": \"Lambda\"\n        },\n        {\n            \"id\": \"39ca9b44-c416-45eb-b2c0-591956bd2fe9\",\n            \"type\": \"PlayPrompt\",\n            \"branches\": [\n                {\n                    \"condition\": \"Success\",\n                    \"transition\": \"406812d0-65de-4f5a-ba33-89c450b94238\"\n                }\n            ],\n            \"parameters\": [\n                {\n                    \"name\": \"AudioPrompt\",\n                    \"value\": \"prompt3\",\n                    \"namespace\": \"External\",\n                    \"resourceName\": null\n                }\n            ],\n            \"metadata\": {\n                \"position\": {\n                    \"x\": 948,\n                    \"y\": 18\n                },\n                \"useDynamic\": true\n            }\n        },\n        {\n            \"id\": \"f5205242-eeb0-4b71-bb47-f8c2adf848fa\",\n            \"type\": \"Disconnect\",\n            \"branches\": [],\n            \"parameters\": [],\n            \"metadata\": {\n                \"position\": {\n                    \"x\": 1442,\n                    \"y\": 22\n                }\n            }\n        },\n        {\n            \"id\": \"406812d0-65de-4f5a-ba33-89c450b94238\",\n            \"type\": \"PlayPrompt\",\n            \"branches\": [\n                {\n                    \"condition\": \"Success\",\n                    \"transition\": \"2298a0bd-cb66-4476-b1cb-1680a079eca6\"\n                }\n            ],\n            \"parameters\": [\n                {\n                    \"name\": \"AudioPrompt\",\n                    \"value\": \"prompt4\",\n                    \"namespace\": \"External\",\n                    \"resourceName\": null\n                }\n            ],\n            \"metadata\": {\n                \"position\": {\n                    \"x\": 1198,\n                    \"y\": 17\n                },\n                \"useDynamic\": true\n            }\n        },\n        {\n            \"id\": \"2298a0bd-cb66-4476-b1cb-1680a079eca6\",\n            \"type\": \"PlayPrompt\",\n            \"branches\": [\n                {\n                    \"condition\": \"Success\",\n                    \"transition\": \"f5205242-eeb0-4b71-bb47-f8c2adf848fa\"\n                }\n            ],\n            \"parameters\": [\n                {\n                    \"name\": \"AudioPrompt\",\n                    \"value\": \"${prompts['a-10-second-silence.wav']}\",\n                    \"namespace\": null,\n                    \"resourceName\": \"a-10-second-silence.wav\"\n                }\n            ],\n            \"metadata\": {\n                \"position\": {\n                    \"x\": 1395,\n                    \"y\": 268\n                },\n                \"useDynamic\": false,\n                \"promptName\": \"a-10-second-silence.wav\"\n            }\n        },\n        {\n            \"id\": \"e30d63b7-e7d5-42df-9dea-f93e0bed321d\",\n            \"type\": \"PlayPrompt\",\n            \"branches\": [\n                {\n                    \"condition\": \"Success\",\n                    \"transition\": \"ad3b6726-dfed-40fe-b4c7-95a9751fc4a7\"\n                }\n            ],\n            \"parameters\": [\n                {\n                    \"name\": \"AudioPrompt\",\n                    \"value\": \"${prompts['a-10-second-silence.wav']}\",\n                    \"namespace\": null,\n                    \"resourceName\": \"a-10-second-silence.wav\"\n                }\n            ],\n            \"metadata\": {\n                \"position\": {\n                    \"x\": 120,\n                    \"y\": 242\n                },\n                \"useDynamic\": false,\n                \"promptName\": \"a-10-second-silence.wav\"\n            }\n        }\n    ],\n    \"version\": \"1\",\n    \"type\": \"contactFlow\",\n    \"start\": \"e30d63b7-e7d5-42df-9dea-f93e0bed321d\",\n    \"metadata\": {\n        \"entryPointPosition\": {\n            \"x\": 24,\n            \"y\": 17\n        },\n        \"snapToGrid\": false,\n        \"name\": \"myFlow\",\n        \"description\": \"An example flow\",\n        \"type\": \"contactFlow\",\n        \"status\": \"published\",\n        \"hash\": \"f8c17f9cd5523dc9c62111e55d2c225e0ee90ad8d509d677429cf6f7f2497a2f\"\n    }\n}`;\n\n    /*fs.writeFileSync(\"/tmp/flow.json\", flow, {\n        mode: 0o777\n    });*/\n\n    LOG.debug(flow);\n\n    await page.waitFor(5000);\n\n    page.click('#import-cf-file-button');\n    let fileinput = await page.$('#import-cf-file');\n    LOG.debug(fileinput);\n    await page.waitFor(1000);\n    await debugScreenshot(page);\n    //await fileinput.uploadFile('/tmp/flow.json'); // broken!\n\n    await page.evaluate((flow) => {\n        angular.element(document.getElementById('import-cf-file')).scope().importContactFlow(new Blob([flow], {type: \"application/json\"}));\n    }, flow);\n    \n    await page.waitFor(5000);\n\n    await debugScreenshot(page);\n\n    await page.click('.header-button'); // Publish\n    await page.waitFor(2000);\n\n    await page.click('awsui-button[text=\"Publish\"] > button'); // Publish modal\n\n    await page.waitFor(8000);\n\n    await debugScreenshot(page);\n}\n\nasync function loginStage1(page, email) {\n    await page.goto('https://console.aws.amazon.com/console/home', {\n        timeout: 0,\n        waitUntil: ['domcontentloaded']\n    });\n    await page.waitForSelector('#resolving_input', {timeout: 15000});\n    await page.waitFor(500);\n\n    LOG.debug(\"Entering email \" + email);\n    let resolvinginput = await page.$('#resolving_input');\n    await resolvinginput.press('Backspace');\n    await resolvinginput.type(email, { delay: 100 });\n\n    await page.click('#next_button');\n\n    await debugScreenshot(page);\n\n    await page.waitFor(5000);\n\n    let captchacontainer = await page.$('#captcha_container');\n    let captchacontainerstyle = await page.evaluate((obj) => {\n        return obj.getAttribute('style');\n    }, captchacontainer);\n\n    var captchanotdone = true;\n    var captchaattempts = 0;\n\n    if (captchacontainerstyle.includes(\"display: none\")) {\n        LOG.debug(\"Skipping login CAPTCHA\");\n    } else {\n        while (captchanotdone) {\n            captchaattempts += 1;\n            if (captchaattempts > 6) {\n                LOG.error(\"Failed CAPTCHA too many times, aborting\");\n                return;\n            }\n            try {\n                let submitc = await page.$('#submit_captcha');\n\n                await debugScreenshot(page);\n                let recaptchaimgx = await page.$('#captcha_image');\n                let recaptchaurlx = await page.evaluate((obj) => {\n                    return obj.getAttribute('src');\n                }, recaptchaimgx);\n\n                LOG.debug(\"CAPTCHA IMG URL:\");\n                LOG.debug(recaptchaurlx);\n                let result = await solveCaptcha(page, recaptchaurlx);\n\n                LOG.debug(\"CAPTCHA RESULT:\");\n                LOG.debug(result);\n\n                let input3 = await page.$('#captchaGuess');\n                await input3.press('Backspace');\n                await input3.type(result, { delay: 100 });\n\n                await debugScreenshot(page);\n                await submitc.click();\n                await page.waitFor(5000);\n\n                await debugScreenshot(page);\n\n                captchacontainer = await page.$('#captcha_container');\n                captchacontainerstyle = await page.evaluate((obj) => {\n                    return obj.getAttribute('style');\n                }, captchacontainer);\n\n                if (captchacontainerstyle.includes(\"display: none\")) {\n                    LOG.debug(\"Successful CAPTCHA solve\");\n\n                    captchanotdone = false;\n                }\n            } catch (error) {\n                LOG.error(error);\n            }\n        }\n\n        await page.waitFor(5000);\n    }\n}\n\nasync function handleEmailInbound(page, event) {\n    for (const record of event['Records']) {\n        var account = null;\n        var email = '';\n        var body = '';\n        var isdeletable = false;\n        \n        let data = await s3.getObject({\n            Bucket: record.s3.bucket.name,\n            Key: record.s3.object.key\n        }).promise();\n        \n        var msg = InternetMessage.parse(data.Body.toString());\n\n        email = msg.to;\n        body = msg.body;\n\n        var emailmatches = /<(.*)>/g.exec(msg.to);\n        if (emailmatches && emailmatches.length > 1) {\n            email = emailmatches[1];\n        }\n\n        data = await retryWrapper(organizations, 'listAccounts', {\n            // no params\n        });\n        let accounts = data.Accounts;\n        while (data.NextToken) {\n            data = await retryWrapper(organizations, 'listAccounts', {\n                NextToken: data.NextToken\n            });\n    \n            accounts = accounts.concat(data.Accounts);\n        }\n    \n        for (const accountitem of accounts) {\n            if (accountitem.Email == email) {\n                account = accountitem;\n            }\n        }\n\n        var accountemailforwardingaddress = null;\n        var provisionedproductid = null;\n\n        if (account) {\n            let orgtags = await retryWrapper(organizations, 'listTagsForResource', { // TODO: paginate\n                ResourceId: account.Id\n            });\n\n            orgtags.Tags.forEach(tag => {\n                if (tag.Key.toLowerCase() == \"delete\" && tag.Value.toLowerCase() == \"true\") {\n                    isdeletable = true;\n                }\n                if (tag.Key.toLowerCase() == \"accountemailforwardingaddress\") {\n                    accountemailforwardingaddress = tag.Value;\n                }\n                if (tag.Key.toLowerCase() == \"accountemailforwardingaddress\") {\n                    accountemailforwardingaddress = tag.Value;\n                }\n                if (tag.Key.toLowerCase() == \"servicecatalogprovisionedproductid\") {\n                    provisionedproductid = tag.Value;\n                }\n            });\n        }\n\n        let filteredbody = body.replace(/=3D/g, '=').replace(/=\\r\\n/g, '');\n\n        let start = filteredbody.indexOf(\"https://signin.aws.amazon.com/resetpassword\");\n        if (start !== -1) {\n            LOG.debug(\"Started processing password reset\");\n\n            let secretsmanagerresponse = await secretsmanager.getSecretValue({\n                SecretId: process.env.SECRET_ARN\n            }).promise();\n\n            let secretdata = JSON.parse(secretsmanagerresponse.SecretString);\n\n            let end = filteredbody.indexOf(\"<\", start);\n            let url = filteredbody.substring(start, end);\n\n            let parsedurl = new URL(url);\n            if (parsedurl.host != \"signin.aws.amazon.com\") { // safety\n                throw \"Unexpected reset password host\";\n            }\n\n            if (!account) { // safety\n                LOG.debug(\"No account found, aborting\");\n                return;\n            }\n\n            LOG.debug(url);\n            \n            await page.goto(url, {\n                timeout: 0,\n                waitUntil: ['domcontentloaded']\n            });\n            await page.waitFor(5000);\n\n            await debugScreenshot(page);\n\n            let newpwinput = await page.$('#new_password');\n            await newpwinput.press('Backspace');\n            await newpwinput.type(secretdata.password, { delay: 100 });\n\n            let input2 = await page.$('#confirm_password');\n            await input2.press('Backspace');\n            await input2.type(secretdata.password, { delay: 100 });\n\n            await page.click('#reset_password_submit');\n            await page.waitFor(5000);\n\n            LOG.info(\"Completed resetpassword link verification\");\n\n            if (isdeletable) {\n                LOG.info(\"Begun delete account\");\n\n                if (provisionedproductid) {\n                    var terminaterecord = await servicecatalog.terminateProvisionedProduct({\n                        TerminateToken: Math.random().toString().substr(2),\n                        IgnoreErrors: true,\n                        ProvisionedProductId: provisionedproductid\n                    }).promise();\n                }\n\n                await loginStage1(page, email);\n\n                await debugScreenshot(page);\n                \n                let input4 = await page.$('#password');\n                await input4.press('Backspace');\n                await input4.type(secretdata.password, { delay: 100 });\n\n                await debugScreenshot(page);\n\n                await page.click('#signin_button');\n                await page.waitFor(8000);\n                \n                await debugScreenshot(page);\n\n                await page.goto('https://portal.aws.amazon.com/billing/signup?client=organizations&enforcePI=True', {\n                    timeout: 0,\n                    waitUntil: ['domcontentloaded']\n                });\n                await page.waitFor(8000);\n                \n                await debugScreenshot(page);\n                LOG.debug(\"Screenshotted at portal\");\n                LOG.debug(page.mainFrame().url());\n                // /confirmation is an activation period\n                if (page.mainFrame().url().split(\"#\").pop() == \"/paymentinformation\") {\n\n                    let input5 = await page.$('#credit-card-number');\n                    await input5.press('Backspace');\n                    await input5.type(secretdata.ccnumber, { delay: 100 });\n\n                    await page.select('#expirationMonth', (parseInt(secretdata.ccmonth)-1).toString());\n\n                    await page.waitFor(2000);\n                    await debugScreenshot(page);\n\n                    let currentyear = new Date().getFullYear();\n\n                    await page.select('select[name=\\'expirationYear\\']', (parseInt(secretdata.ccyear)-currentyear).toString());\n\n                    let input6 = await page.$('#accountHolderName');\n                    await input6.press('Backspace');\n                    await input6.type(secretdata.ccname, { delay: 100 });\n\n                    await page.waitFor(2000);\n                    await debugScreenshot(page);\n\n                    await page.click('.form-submit-click-box > button');\n\n                    await page.waitFor(8000);\n                }\n\n                await debugScreenshot(page);\n\n                if (page.mainFrame().url().split(\"#\").pop() == \"/identityverification\") {\n                    let usoption = await page.$('option[label=\"United States (+1)\"]');\n                    let usvalue = await page.evaluate( (obj) => {\n                        return obj.getAttribute('value');\n                    }, usoption);\n\n                    await page.select('#countryCode', usvalue);\n\n                    let connectssmparameter = await ssm.getParameter({\n                        Name: process.env.CONNECT_SSM_PARAMETER\n                    }).promise();\n\n                    let variables = JSON.parse(connectssmparameter['Parameter']['Value']);\n\n                    let portalphonenumber = await page.$('#phoneNumber');\n                    await portalphonenumber.press('Backspace');\n                    await portalphonenumber.type(variables['PHONE_NUMBER'].replace(\"+1\", \"\"), { delay: 100 });\n\n                    var phonecode = \"\";\n                    var phonecodetext = \"\";\n                    var captchanotdone = true;\n                    var captchaattemptsfordiva = 0;\n                    while (captchanotdone) {\n                        captchaattemptsfordiva += 1;\n                        if (captchaattemptsfordiva > 5) {\n                            throw \"Could not confirm phone number verification - possible error in DIVA system or credit card\";\n                        }\n                        try {\n                            let submitc = await page.$('#btnCall');\n\n                            await debugScreenshot(page);\n                            let recaptchaimgx = await page.$('#imageCaptcha');\n                            let recaptchaurlx = await page.evaluate((obj) => {\n                                return obj.getAttribute('src');\n                            }, recaptchaimgx);\n\n                            LOG.debug(\"CAPTCHA IMG URL:\");\n                            LOG.debug(recaptchaurlx);\n                            let result = await solveCaptcha(page, recaptchaurlx);\n\n                            LOG.debug(\"CAPTCHA RESULT:\");\n                            LOG.debug(result);\n\n                            let input32 = await page.$('#guess');\n                            await input32.press('Backspace');\n                            await input32.type(result, { delay: 100 });\n\n                            await debugScreenshot(page);\n                            await submitc.click();\n                            await page.waitFor(5000);\n\n                            await debugScreenshot(page);\n\n                            await page.waitForSelector('.phone-pin-number', {timeout: 5000});\n\n                            phonecode = await page.$('.phone-pin-number > span');\n                            phonecodetext = await page.evaluate(el => el.textContent, phonecode);\n\n                            if (phonecodetext.trim().length == 4) {\n                                captchanotdone = false;\n                            } else {\n                                await page.waitFor(5000);\n                            }\n                        } catch (error) {\n                            LOG.error(error);\n                        }\n                    }\n\n                    await debugScreenshot(page);\n                                \n                    variables['CODE'] = phonecodetext;\n    \n                    await ssm.putParameter({\n                        Name: process.env.CONNECT_SSM_PARAMETER,\n                        Type: \"String\",\n                        Value: JSON.stringify(variables),\n                        Overwrite: true\n                    }).promise();\n\n                    await page.waitFor(30000);\n                    \n                    await debugScreenshot(page);\n\n                    try {\n                        await page.click('#verification-complete-button');\n                    } catch(err) {\n                        LOG.error(\"Could not confirm phone number verification - possible error in DIVA system or credit card\");\n                        throw err;\n                    }\n\n                    await page.waitFor(3000);\n                    \n                    await debugScreenshot(page);\n\n                }\n\n                if (page.mainFrame().url().split(\"#\").pop() == \"/support\" || page.mainFrame().url().split(\"#\").pop() == \"/confirmation\") {\n                    await page.goto('https://console.aws.amazon.com/billing/rest/v1.0/account', {\n                        timeout: 0,\n                        waitUntil: ['domcontentloaded']\n                    });\n\n                    await page.waitFor(3000);\n\n                    await debugScreenshot(page);\n\n                    let accountstatuspage = await page.content();\n\n                    LOG.debug(accountstatuspage);\n\n                    let issuspended = accountstatuspage.includes(\"\\\"accountStatus\\\":\\\"Suspended\\\"\");\n\n                    if (provisionedproductid) {\n                        let terminatestatus = \"CREATED\";\n                        while (['CREATED', 'IN_PROGRESS'].includes(terminatestatus)) {\n                            await new Promise((resolve) => {setTimeout(resolve, 10000)});\n\n                            let record = await servicecatalog.describeRecord({\n                                Id: terminaterecord.RecordDetail.RecordId\n                            }).promise();\n                            terminatestatus = record.RecordDetail.Status;\n                        }\n                        if (terminatestatus != \"SUCCEEDED\") {\n                            throw \"Could not terminate product from Service Catalog\";\n                        }\n                    }\n\n                    if (!issuspended) {\n                        await page.goto('https://console.aws.amazon.com/billing/home?#/account', {\n                            timeout: 0,\n                            waitUntil: ['domcontentloaded']\n                        });\n\n                        await page.waitFor(8000);\n\n                        await debugScreenshot(page);\n\n                        let closeaccountcbs = await page.$$('.close-account-checkbox > input');\n                        await closeaccountcbs.forEach(async (cb) => {\n                            await cb.click();\n                        });\n\n                        await page.waitFor(1000);\n\n                        await debugScreenshot(page);\n\n                        await page.click('.btn-danger'); // close account button\n\n                        await page.waitFor(1000);\n\n                        await debugScreenshot(page);\n\n                        await page.click('.modal-footer > button.btn-danger'); // confirm close account button\n\n                        await page.waitFor(5000);\n\n                        await debugScreenshot(page);\n\n                        await retryWrapper(organizations, 'tagResource', {\n                            ResourceId: account.Id,\n                            Tags: [{\n                                Key: \"AccountDeletionTime\",\n                                Value: (new Date()).toISOString()\n                            }]\n                        });\n                    }\n\n                    await removeAccountFromOrg(account);\n                } else {\n                    LOG.warn(\"Unsure of location, send help! - \" + page.mainFrame().url());\n                }\n            }\n            \n        } else {\n            LOG.debug(\"No password reset found, forwarding e-mail\");\n\n            await new Promise(async (resolve, reject) => {\n                var accountid = \"?\";\n                var accountemail = \"?\";\n                var accountname= \"?\";\n                if (account) {\n                    accountid = account.Id || \"?\";\n                    accountemail = account.Email || \"?\";\n                    accountname = account.Name || \"?\";\n                }\n                var msgsubject = msg.subject || \"\";\n                var from = msg.from || \"\";\n                var to = msg.to || \"\";\n    \n                msg.subject = process.env.EMAIL_SUBJECT.\n                    replace(\"{subject}\", msgsubject).\n                    replace(\"{from}\", from).\n                    replace(\"{to}\", to).\n                    replace(\"{accountid}\", accountid).\n                    replace(\"{accountname}\", accountname).\n                    replace(\"{accountemail}\", accountemail);\n    \n                msg.to = accountemailforwardingaddress || \"AWS Accounts Master <\" + MASTER_EMAIL + \">\";\n                msg.from = \"AWS Accounts Master <\" + MASTER_EMAIL + \">\";\n                msg['return-path'] = \"AWS Accounts Master <\" + MASTER_EMAIL + \">\";\n    \n                var stringified = InternetMessage.stringify(msg);\n                \n                ses.sendRawEmail({\n                    Source: MASTER_EMAIL,\n                    Destinations: [msg.to],\n                    RawMessage: {\n                        Data: stringified\n                    }\n                }, async function (err, data) {\n                    if (err) {\n                        LOG.debug(err);\n    \n                        msg.to = \"AWS Accounts Master <\" + MASTER_EMAIL + \">\";\n                        \n                        await ses.sendRawEmail({\n                            Source: MASTER_EMAIL,\n                            Destinations: [MASTER_EMAIL],\n                            RawMessage: {\n                                Data: \"To: \" + msg.to + \"\\r\\nFrom: \" + msg.from + \"\\r\\nSubject: \" + msg.subject + \"\\r\\n\\r\\n***CONTENT NOT PROCESSABLE***\\r\\n\\r\\nDownload the email from s3://\" + record.s3.bucket.name + \"/\" + record.s3.object.key + \"\\r\\n\"\n                            }\n                        }).promise();\n                    }\n    \n                    resolve();\n                });\n            });\n        }\n    }\n    \n    return true;\n};\n\nasync function removeAccountFromOrg(account) {\n    var now = new Date();\n    var threshold = new Date(account.JoinedTimestamp);\n    threshold.setDate(threshold.getDate() + 7); // 7 days\n    if (now > threshold) {\n        await retryWrapper(organizations, 'removeAccountFromOrganization', {\n            AccountId: account.Id\n        });\n\n        LOG.info(\"Removed account from Org\");\n\n        return true;\n    } else {\n        threshold.setMinutes(threshold.getMinutes() + 2); // plus 2 minutes buffer\n        await eventbridge.putRule({\n            Name: \"ScheduledAccountDeletion-\" + account.Id.toString(),\n            Description: \"The scheduled deletion of an Organizations account\",\n            //RoleArn: '',\n            ScheduleExpression: \"cron(\" + threshold.getMinutes() + \" \" + threshold.getUTCHours() + \" \" + threshold.getUTCDate() + \" \" + (threshold.getUTCMonth() + 1) + \" ? \" + threshold.getUTCFullYear() + \")\",\n            State: \"ENABLED\"\n        }).promise();\n\n        await eventbridge.putTargets({\n            Rule: \"ScheduledAccountDeletion-\" + account.Id.toString(),\n            Targets: [{\n                Arn: \"arn:aws:lambda:\" + process.env.AWS_REGION + \":\" + process.env.ACCOUNTID  + \":function:\" + process.env.AWS_LAMBDA_FUNCTION_NAME,\n                Id: \"Lambda\",\n                //RoleArn: \"\",\n                Input: JSON.stringify({\n                    \"action\": \"removeAccountFromOrg\",\n                    \"account\": account,\n                    \"ruleName\": \"ScheduledAccountDeletion-\" + account.Id.toString()\n                })\n            }]\n        }).promise();\n\n        await retryWrapper(organizations, 'tagResource', {\n            ResourceId: account.Id,\n            Tags: [{\n                Key: \"ScheduledRemovalTime\",\n                Value: threshold.toISOString()\n            }]\n        });\n\n        LOG.info(\"Scheduled removal for later\");\n    }\n\n    return false;\n}\n\nasync function triggerReset(page, event) {\n    await loginStage1(page, event.email);\n    \n    await debugScreenshot(page);\n\n    await page.click('#root_forgot_password_link');\n\n    await page.waitFor(2000);\n\n    await page.waitForSelector('#password_recovery_captcha_image', {timeout: 15000});\n\n    captchanotdone = true;\n    captchaattempts = 0;\n    while (captchanotdone) {\n        captchaattempts += 1;\n        if (captchaattempts > 6) {\n            LOG.error(\"Failed CAPTCHA too many times, aborting\");\n            return;\n        }\n\n        await debugScreenshot(page);\n\n        let recaptchaimg = await page.$('#password_recovery_captcha_image');\n        let recaptchaurl = await page.evaluate((obj) => {\n            return obj.getAttribute('src');\n        }, recaptchaimg);\n\n        LOG.debug(recaptchaurl);\n        let captcharesult = await solveCaptcha(page, recaptchaurl);\n\n        let input2 = await page.$('#password_recovery_captcha_guess');\n        await input2.press('Backspace');\n        await input2.type(captcharesult, { delay: 100 });\n\n        await page.waitFor(3000);\n\n        await debugScreenshot(page);\n\n        await page.click('#password_recovery_ok_button');\n\n        await page.waitFor(5000);\n\n        let errormessagediv = await page.$('#password_recovery_error_message');\n        let errormessagedivstyle = await page.evaluate((obj) => {\n            return obj.getAttribute('style');\n        }, errormessagediv);\n        \n        if (errormessagedivstyle.includes(\"display: none\")) {\n            captchanotdone = false;\n        }\n    }\n\n    await debugScreenshot(page);\n\n    await page.waitFor(2000);\n};\n\nasync function addSubscriptionsSCP(details) {\n    LOG.info(\"Adding subscriptions SCP\");\n\n    let rolename = 'OrganizationAccountAccessRole';\n    if (process.env.CONTROL_TOWER_MODE == \"true\") {\n        rolename = 'AWSControlTowerExecution';\n    }\n\n    let policyid = null;\n    let policiesdata = await retryWrapper(organizations, 'listPolicies', {\n        Filter: 'SERVICE_CONTROL_POLICY'\n    });\n    let policies = policiesdata.Policies;\n\n    while (policiesdata.NextToken) {\n        policiesdata = await retryWrapper(organizations, 'listPolicies', {\n            Filter: 'SERVICE_CONTROL_POLICY',\n            NextToken: policiesdata.NextToken\n        });\n        policies.concat(policiesdata.Policies);\n    }\n\n    policyid = null;\n\n    for (const policy of policies) {\n        if (policy.Name == \"AccountManagerDenySubscriptionCalls\") {\n            policyid = policy.Id;\n        }\n    }\n    \n    if (!policyid) {\n        policydata = await retryWrapper(organizations, 'createPolicy', {\n            Content: JSON.stringify({\n                Version: \"2012-10-17\",\n                Statement: {\n                    Effect: \"Deny\",\n                    Action: [\n                        \"route53domains:RegisterDomain\",\n                        \"route53domains:RenewDomain\",\n                        \"route53domains:TransferDomain\",\n                        \"ec2:ModifyReservedInstances\",\n                        \"ec2:PurchaseHostReservation\",\n                        \"ec2:PurchaseReservedInstancesOffering\",\n                        \"ec2:PurchaseScheduledInstances\",\n                        \"rds:PurchaseReservedDBInstancesOffering\",\n                        \"dynamodb:PurchaseReservedCapacityOfferings\",\n                        \"s3:PutObjectRetention\",\n                        \"s3:PutObjectLegalHold\",\n                        \"s3:BypassGovernanceRetention\",\n                        \"s3:PutBucketObjectLockConfiguration\",\n                        \"elasticache:PurchaseReservedCacheNodesOffering\",\n                        \"redshift:PurchaseReservedNodeOffering\",\n                        \"savingsplans:CreateSavingsPlan\",\n                        \"aws-marketplace:AcceptAgreementApprovalRequest\",\n                        \"aws-marketplace:Subscribe\"\n                    ],\n                    Resource: \"*\",\n                    Condition: {\n                        StringNotLike: {\n                            'aws:PrincipalArn': 'arn:aws:iam::*:role/' + rolename\n                        }\n                    }\n                }\n            }),\n            Description: 'Used to restrict access to create long-term subscriptions',\n            Name: 'AccountManagerDenySubscriptionCalls',\n            Type: 'SERVICE_CONTROL_POLICY'\n        });\n        \n        policyid = policydata.Policy.PolicySummary.Id;\n    } else {\n        await retryWrapper(organizations, 'updatePolicy', {\n            Content: JSON.stringify({\n                Version: \"2012-10-17\",\n                Statement: {\n                    Effect: \"Deny\",\n                    Action: [\n                        \"route53domains:RegisterDomain\",\n                        \"route53domains:RenewDomain\",\n                        \"route53domains:TransferDomain\",\n                        \"ec2:ModifyReservedInstances\",\n                        \"ec2:PurchaseHostReservation\",\n                        \"ec2:PurchaseReservedInstancesOffering\",\n                        \"ec2:PurchaseScheduledInstances\",\n                        \"rds:PurchaseReservedDBInstancesOffering\",\n                        \"dynamodb:PurchaseReservedCapacityOfferings\",\n                        \"s3:PutObjectRetention\",\n                        \"s3:PutObjectLegalHold\",\n                        \"s3:BypassGovernanceRetention\",\n                        \"s3:PutBucketObjectLockConfiguration\",\n                        \"elasticache:PurchaseReservedCacheNodesOffering\",\n                        \"redshift:PurchaseReservedNodeOffering\",\n                        \"savingsplans:CreateSavingsPlan\",\n                        \"aws-marketplace:AcceptAgreementApprovalRequest\",\n                        \"aws-marketplace:Subscribe\",\n                        \"shield:CreateSubscription\"\n                    ],\n                    Resource: \"*\",\n                    Condition: {\n                        StringNotLike: {\n                            'aws:PrincipalArn': 'arn:aws:iam::*:role/' + rolename\n                        }\n                    }\n                }\n            }),\n            PolicyId: policyid\n        }).catch(() => {});\n    }\n\n    await retryWrapper(organizations, 'attachPolicy', {\n        PolicyId: policyid,\n        TargetId: details['accountid']\n    }).catch(err => {\n        if (err.code == \"DuplicatePolicyAttachmentException\") {\n            LOG.info(\"Skipping attach subscription SCP, already attached\");\n        } else {\n            throw err;\n        }\n    });\n}\n\nasync function addBillingMonitor(page, details) {\n    LOG.info(\"Adding billing monitor\");\n    \n    let rolename = 'OrganizationAccountAccessRole';\n    if (process.env.CONTROL_TOWER_MODE == \"true\") {\n        rolename = 'AWSControlTowerExecution';\n    }\n\n    let assumedrole = await sts.assumeRole({\n        RoleArn: 'arn:aws:iam::' + details['accountid'] + ':role/' + rolename,\n        RoleSessionName: 'AccountManagerAddBillingMonitor'\n    }).promise();\n\n    let policyid = null;\n    let policiesdata = await retryWrapper(organizations, 'listPolicies', {\n        Filter: 'SERVICE_CONTROL_POLICY'\n    });\n    let policies = policiesdata.Policies;\n\n    while (policiesdata.NextToken) {\n        policiesdata = await retryWrapper(organizations, 'listPolicies', {\n            Filter: 'SERVICE_CONTROL_POLICY',\n            NextToken: policiesdata.NextToken\n        });\n        policies.concat(policiesdata.Policies);\n    }\n\n    for (const policy of policies) {\n        if (policy.Name == \"AccountManagerDenyBillingAlarmAccess\") {\n            policyid = policy.Id;\n        }\n    }\n    \n    if (!policyid) {\n        policydata = await retryWrapper(organizations, 'createPolicy', {\n            Content: JSON.stringify({\n                Version: \"2012-10-17\",\n                Statement: {\n                    Effect: \"Deny\",\n                    Action: \"*\",\n                    Resource: \"arn:aws:cloudwatch:us-east-1:*:alarm:AccountManagerDeletionBudgetMonitor\",\n                    Condition: {\n                        StringNotLike: {\n                            'aws:PrincipalArn': 'arn:aws:iam::*:role/' + rolename\n                        }\n                    }\n                }\n            }),\n            Description: 'Used to restrict access to the billing alarm',\n            Name: 'AccountManagerDenyBillingAlarmAccess',\n            Type: 'SERVICE_CONTROL_POLICY'\n        });\n        \n        policyid = policydata.Policy.PolicySummary.Id;\n    } else {\n        await retryWrapper(organizations, 'updatePolicy', {\n            Content: JSON.stringify({\n                Version: \"2012-10-17\",\n                Statement: {\n                    Effect: \"Deny\",\n                    Action: \"*\",\n                    Resource: \"arn:aws:cloudwatch:us-east-1:*:alarm:AccountManagerDeletionBudgetMonitor\",\n                    Condition: {\n                        StringNotLike: {\n                            'aws:PrincipalArn': 'arn:aws:iam::*:role/' + rolename\n                        }\n                    }\n                }\n            }),\n            PolicyId: policyid\n        }).catch(() => { });\n    }\n\n    await retryWrapper(organizations, 'attachPolicy', {\n        PolicyId: policyid,\n        TargetId: details['accountid']\n    }).catch(err => {\n        if (err.code == \"DuplicatePolicyAttachmentException\") {\n            LOG.info(\"Skipping attach billing SCP, already attached\");\n        } else {\n            throw err;\n        }\n    });\n\n    //await new Promise((resolve) => {setTimeout(resolve, 120000)}); // wait for account active\n\n    let childcloudwatch = new AWS.CloudWatch({\n        accessKeyId: assumedrole.Credentials.AccessKeyId,\n        secretAccessKey: assumedrole.Credentials.SecretAccessKey,\n        sessionToken: assumedrole.Credentials.SessionToken\n    });\n\n    let alarm = await retryWrapper(childcloudwatch, 'putMetricAlarm', {\n        AlarmName: 'AccountManagerDeletionBudgetMonitor',\n        ComparisonOperator: 'GreaterThanThreshold',\n        EvaluationPeriods: 1,\n        ActionsEnabled: true,\n        AlarmActions: [\n            process.env.ACCOUNT_DELETION_TOPIC\n        ],\n        AlarmDescription: 'Sends a request to delete this account to the account manager when the budget is reached',\n        DatapointsToAlarm: 1,\n        Dimensions: [{\n            Name: 'Currency',\n            Value: 'USD'\n        }],\n        MetricName: 'EstimatedCharges',\n        Namespace: 'AWS/Billing',\n        Period: 21600,\n        Statistic: 'Maximum',\n        Threshold: details['budgetthresholdbeforedeletion'],\n        TreatMissingData: 'ignore',\n        Unit: 'None'\n    }); // subject to OptInRequired\n\n    LOG.debug(alarm);\n    LOG.info(\"Completed adding billing monitor\");\n}\n\nasync function setSSOOwner(page, details) {\n    let ssoparamresponse = await ssm.getParameter({\n        Name: process.env.SSO_SSM_PARAMETER\n    }).promise();\n\n    let ssoproperties = JSON.parse(ssoparamresponse['Parameter']['Value']);\n\n    await page.goto('https://console.aws.amazon.com/singlesignon/home?region=' + process.env.AWS_REGION + '#/accounts/organization/assignUsers?ids=' + details['accountid'] + '&step=userGroupsStep', {\n        timeout: 0,\n        waitUntil: ['domcontentloaded']\n    });\n\n    await page.waitFor(5000);\n\n    await debugScreenshot(page);\n\n    const cookies = await page.cookies();\n\n    let cookie = \"\";\n    cookies.forEach(cookieitem => {\n        cookie += cookieitem['name'] + \"=\" + cookieitem['value'] + \"; \";\n    });\n    cookie = cookie.substr(0, cookie.length - 2);\n\n    let csrftoken = await page.$eval('head > meta[name=\"awsc-csrf-token\"]', element => element.content);\n\n    //--//\n    \n    let directoryConfig = await rp({\n        uri: 'https://console.aws.amazon.com/singlesignon/api/peregrine',\n        method: 'POST',\n        body: JSON.stringify({\n            \"method\": \"POST\",\n            \"path\": \"/control/\",\n            \"headers\": {\n                \"Content-Type\": \"application/json; charset=UTF-8\",\n                \"Content-Encoding\": \"amz-1.0\",\n                \"X-Amz-Target\": \"com.amazon.switchboard.service.SWBService.ListDirectoryAssociations\",\n                \"X-Amz-Date\": dateFormat(new Date(), \"GMT:ddd, dd mmm yyyy HH:MM:ss\") + \" GMT\",\n                \"Accept\": \"application/json, text/javascript, */*\"\n            },\n            \"region\": \"us-east-1\",\n            \"operation\": \"ListDirectoryAssociations\",\n            \"contentString\": JSON.stringify({\n                \"marker\": null\n            })\n        }),\n        headers: {\n            'accept': 'application/json, text/plain, */*',\n            'content-type': 'application/json',\n            'x-csrf-token': csrftoken,\n            'cookie': cookie\n        }\n    });\n\n    let primaryDirectoryId = JSON.parse(directoryConfig).directoryAssociations[0].directoryId;\n\n    let userConfig = await rp({\n        uri: 'https://console.aws.amazon.com/singlesignon/api/identitystore',\n        method: 'POST',\n        body: JSON.stringify({\n            \"method\": \"POST\",\n            \"path\": \"/identitystore/\",\n            \"headers\": {\n                \"Content-Type\": \"application/json; charset=UTF-8\",\n                \"Content-Encoding\": \"amz-1.0\",\n                \"X-Amz-Target\": \"com.amazonaws.identitystore.AWSIdentityStoreService.DescribeUsers\",\n                \"X-Amz-Date\": \"Wed, 08 Apr 2020 02:22:19 GMT\",\n                \"Accept\": \"application/json, text/javascript, */*\"\n            },\n            \"region\":\"us-east-1\",\n            \"operation\":\"DescribeUsers\",\n            \"contentString\": JSON.stringify({\n                \"IdentityStoreId\": primaryDirectoryId,\n                \"UserIds\": [\n                    details['accountowner']\n                ]\n            })\n        }),\n        headers: {\n            'accept': 'application/json, text/plain, */*',\n            'content-type': 'application/json',\n            'x-csrf-token': csrftoken,\n            'cookie': cookie\n        }\n    });\n\n    let username = JSON.parse(userConfig).Users[0].UserName;\n\n    await page.click('awsui-select[ng-model=\"table.controlValues.selectedSearchValue\"]');\n    await page.waitFor(200);\n\n    await page.click('li[data-value=\"userName\"]');\n    await page.waitFor(200);\n\n    await debugScreenshot(page);\n\n    let usernamesearch = await page.$('awsui-textfield[ng-model=\"table.controlValues.search\"] > input');\n    await usernamesearch.press('Backspace');\n    await usernamesearch.type(username, { delay: 100 });\n\n    await page.waitFor(5000);\n\n    await page.click('.select-all > .checkbox > awsui-checkbox');\n    await page.waitFor(200);\n\n    await debugScreenshot(page);\n\n    if (details['isshared']) {\n        LOG.debug(\"Sharing account with group\");\n\n        let paneltabs = await page.$$('.awsui-tabs-tab > a');\n        await paneltabs[1].click();\n        await page.waitFor(5000);\n\n        await debugScreenshot(page);\n        \n        let groupsearch = await page.$('input[placeholder=\"Find groups by name\"]'); // TODO: use a better selector\n        await groupsearch.press('Backspace');\n        await groupsearch.type('AccountManagerUsers', { delay: 100 });\n        await page.waitFor(5000);\n\n        await debugScreenshot(page);\n        \n        await page.click('div.group-name > div.selection > div.checkbox > awsui-checkbox');\n        await page.waitFor(200);\n\n        await debugScreenshot(page);\n    }\n\n    await page.click('.wizard-next-button');\n    await page.waitFor(3000);\n\n    let adminlabel = await page.$('div.cell-content > truncate[tooltip=\"AdministratorAccess\"]');\n    await page.evaluate((obj) => {\n        obj.parentNode.parentNode.querySelector('div.selection > div.checkbox > awsui-checkbox').click();\n    }, adminlabel);\n    await page.waitFor(200);\n\n    await page.click('.wizard-next-button');\n    await page.waitFor(10000);\n\n    await debugScreenshot(page);\n\n    await retryWrapper(organizations, 'tagResource', {\n        ResourceId: details['accountid'],\n        Tags: [{\n            Key: \"SSOCreationComplete\",\n            Value: \"true\"\n        }]\n    });\n}\n\nasync function decodeSAMLResponse(sp, idp, samlresponse) {\n    let resp = await new Promise((resolve,reject) => {\n        sp.post_assert(idp, {\n            request_body: {\n                'SAMLResponse': samlresponse\n            }\n        }, function(err, resp) {\n            if (err) {\n                reject(err);\n            } else {\n                resolve(resp);\n            }\n        });\n    });\n    \n    return resp;\n}\n\nfunction decodeForm(form) {\n    var ret = {};\n\n    var items = form.split(\"&\");\n    items.forEach(item => {\n        var split = item.split(\"=\");\n        ret[split.shift()] = split.join(\"=\");\n    });\n\n    return ret\n}\n\nasync function getUserBySAML(samlresponse) {\n    let ssoparamresponse = await ssm.getParameter({\n        Name: process.env.SSO_SSM_PARAMETER\n    }).promise();\n\n    let ssoproperties = JSON.parse(ssoparamresponse['Parameter']['Value']);\n    \n    var sp_options = {\n        entity_id: \"https://\" + process.env.DOMAIN_NAME + \"/metadata.xml\",\n        private_key: \"\",\n        certificate: \"\",\n        assert_endpoint: \"\",\n        allow_unencrypted_assertion: true\n    };\n    var sp = new saml2.ServiceProvider(sp_options);\n    \n    var idp_options = {\n        sso_login_url: ssoproperties['SignInURL'],\n        sso_logout_url: ssoproperties['SignOutURL'],\n        certificates: [ssoproperties['Certificate']],\n        allow_unencrypted_assertion: true\n    };\n    var idp = new saml2.IdentityProvider(idp_options);\n\n    let samlattrs = await decodeSAMLResponse(sp, idp, decodeURIComponent(samlresponse));\n\n    return {\n        'name': samlattrs['user']['attributes']['name'][0],\n        'email': samlattrs['user']['attributes']['email'][0],\n        'guid': samlattrs['user']['attributes']['guid'][0],\n        'samlresponse': decodeURIComponent(samlresponse),\n        'ssoprops': ssoproperties\n    };\n}\n\nasync function handleSAMLResponse(event) {\n    let body = event.body;\n    if (event.isBase64Encoded) {\n        body = Buffer.from(event.body, 'base64').toString('utf8');\n    }\n\n    var form = decodeForm(body);\n\n    let user = await getUserBySAML(form['SAMLResponse']);\n\n    return {\n        \"statusCode\": 200,\n        \"isBase64Encoded\": false,\n        \"headers\": {\n            \"Content-Type\": \"text/html\"\n        },\n        \"body\": wrapHTML(user)\n    };\n}\n\nasync function handleGetAccounts(event) {\n    let body = event.body;\n    if (event.isBase64Encoded) {\n        body = Buffer.from(event.body, 'base64').toString('utf8');\n    }\n\n    var form = decodeForm(body);\n\n    let user = await getUserBySAML(form['SAMLResponse']);\n\n    let useraccounts = [];\n\n    let data = await retryWrapper(organizations, 'listAccounts', {\n        // no params\n    });\n    let accounts = data.Accounts;\n    while (data.NextToken) {\n        let moreaccounts = await retryWrapper(organizations, 'listAccounts', {\n            NextToken: data.NextToken\n        });\n\n        accounts = accounts.concat(moreaccounts.Accounts);\n    }\n\n    for (const account of accounts) {\n        let tags = await retryWrapper(organizations, 'listTagsForResource', { // TODO: paginate\n            ResourceId: account.Id\n        });\n\n        let shouldAddToUserAccountsList = false;\n        let isdeleted = false;\n        let useraccount = {\n            'Id': account.Id,\n            'Email': account.Email,\n            'JoinedTimestamp': account.JoinedTimestamp,\n            'Name': account.Name\n        };\n        for (const tag of tags.Tags) {\n            if (tag.Key.toLowerCase() == \"notes\") {\n                useraccount['Notes'] = tag.Value.replace(/\\+/g, \" \");\n            }\n            if (tag.Key.toLowerCase() == \"delete\" && tag.Value.toLowerCase() == \"true\") {\n                useraccount['IsDeleting'] = true;\n            }\n            if (tag.Key.toLowerCase() == \"ssocreationcomplete\" && tag.Value.toLowerCase() == \"false\") {\n                useraccount['IsCreating'] = true;\n            }\n            if (tag.Key.toLowerCase() == \"accountownerguid\" && tag.Value == user.guid) {\n                shouldAddToUserAccountsList = true;\n                useraccount['IsOwner'] = true;\n            }\n            if (tag.Key.toLowerCase() == \"sharedwithorg\" && tag.Value.toLowerCase() == \"true\") {\n                shouldAddToUserAccountsList = true;\n                useraccount['IsShared'] = true;\n            }\n            if (tag.Key.toLowerCase() == \"scheduledremovaltime\") {\n                isdeleted = true;\n            }\n        }\n        if (shouldAddToUserAccountsList && !isdeleted) { // ignore deleting, suspended accounts (deferred org removal)\n            useraccounts.push(useraccount);\n        }\n    }\n\n    useraccounts.sort(function(x, y) {\n        return y.JoinedTimestamp - x.JoinedTimestamp;\n    });\n\n    return {\n        \"statusCode\": 200,\n        \"isBase64Encoded\": false,\n        \"headers\": {\n            \"Content-Type\": \"application/json\"\n        },\n        \"body\": JSON.stringify({\n            'accounts': useraccounts\n        })\n    };\n}\n\nasync function processSnsDeleteAccount(event) {\n    for (const record of event['Records']) {\n        if (record.EventSubscriptionArn.startsWith(process.env.ACCOUNT_DELETION_TOPIC)) {\n            let snsmessage = JSON.parse(record.Sns.Message);\n\n            let accountid = snsmessage.AWSAccountId;\n\n            let account = await retryWrapper(organizations, 'describeAccount', {\n                AccountId: accountid\n            });\n\n            LOG.info(\"Deleting account \" + accountid + \" due to budget alert\");\n\n            await retryWrapper(organizations, 'tagResource', {\n                ResourceId: account.Account.Id,\n                Tags: [{\n                    Key: \"Delete\",\n                    Value: \"true\"\n                }]\n            });\n        }\n    }\n}\n\nasync function handleDeleteAccountRequest(event) {\n    let body = event.body;\n    if (event.isBase64Encoded) {\n        body = Buffer.from(event.body, 'base64').toString('utf8');\n    }\n\n    var form = decodeForm(body);\n\n    let user = await getUserBySAML(form['SAMLResponse']);\n\n    let account = await retryWrapper(organizations, 'describeAccount', {\n        AccountId: form['accountid']\n    }).catch(err => {\n        LOG.debug(err);\n\n        return {\n            \"statusCode\": 404,\n            \"isBase64Encoded\": false,\n            \"headers\": {\n                \"Content-Type\": \"application/json\"\n            },\n            \"body\": JSON.stringify({\n                'deleteAccountSuccess': false\n            })\n        };\n    });\n    \n    let tagdata = await retryWrapper(organizations, 'listTagsForResource', {\n        ResourceId: account.Account.Id\n    });\n\n    for (const tag of tagdata.Tags) {\n        if (tag.Key.toLowerCase() == \"accountownerguid\" && tag.Value == user.guid) {\n            await retryWrapper(organizations, 'tagResource', {\n                ResourceId: account.Account.Id,\n                Tags: [{\n                    Key: \"Delete\",\n                    Value: \"true\"\n                }]\n            });\n\n            return {\n                \"statusCode\": 200,\n                \"isBase64Encoded\": false,\n                \"headers\": {\n                    \"Content-Type\": \"application/json\"\n                },\n                \"body\": JSON.stringify({\n                    'deleteAccountSuccess': true\n                })\n            };\n        }\n    }\n\n    return {\n        \"statusCode\": 403,\n        \"isBase64Encoded\": false,\n        \"headers\": {\n            \"Content-Type\": \"application/json\"\n        },\n        \"body\": JSON.stringify({\n            'deleteAccountSuccess': false\n        })\n    };\n}\n\nasync function handleCreateAccountRequest(event) {\n    let body = event.body;\n    if (event.isBase64Encoded) {\n        body = Buffer.from(event.body, 'base64').toString('utf8');\n    }\n\n    var form = decodeForm(body);\n\n    let user = await getUserBySAML(form['SAMLResponse']);\n\n    let accountemail = decodeURIComponent(form['emailprefix'].replace(/\\+/g, ' ')) + \"@\" + process.env.DOMAIN_NAME;\n    let accountname = decodeURIComponent(form['accountname'].replace(/\\+/g, ' '));\n    let notes = decodeURIComponent(form['notes'].replace(/\\ /g, '+'));\n    let maximumspend = \"\";\n    if (form['maximumspend']) {\n        maximumspend = decodeURIComponent(form['maximumspend']);\n    }\n\n    if (!accountname.match(/^.{1,50}$/g)) {\n        return {\n            \"statusCode\": 400,\n            \"isBase64Encoded\": false,\n            \"headers\": {\n                \"Content-Type\": \"application/json\"\n            },\n            \"body\": JSON.stringify({\n                'createAccountSuccess': false,\n                'reason': 'Please enter an account name that is from 1 to 50 characters long'\n            })\n        };\n    }\n\n    if (!accountemail.match(/^.{6,64}$/g)) {\n        return {\n            \"statusCode\": 400,\n            \"isBase64Encoded\": false,\n            \"headers\": {\n                \"Content-Type\": \"application/json\"\n            },\n            \"body\": JSON.stringify({\n                'createAccountSuccess': false,\n                'reason': 'Please enter a valid email address that is from 6 to 64 characters long'\n            })\n        };\n    }\n\n    if (!notes.match(/^[a-zA-Z0-9\\.\\:\\+\\=@_\\/\\-]{0,256}$/g)) {\n        return {\n            \"statusCode\": 400,\n            \"isBase64Encoded\": false,\n            \"headers\": {\n                \"Content-Type\": \"application/json\"\n            },\n            \"body\": JSON.stringify({\n                'createAccountSuccess': false,\n                'reason': 'The notes field can have up to 256 characters (valid characters: a-z, A-Z, 0-9, and . : = @ _ / - <space> )'\n            })\n        };\n    }\n\n    if (process.env.MAXIMUM_ACCOUNT_SPEND != \"0\") {\n        if (!maximumspend.match(/^[0-9]+(?:\\.[0-9]{2})?$/g)) {\n            return {\n                \"statusCode\": 400,\n                \"isBase64Encoded\": false,\n                \"headers\": {\n                    \"Content-Type\": \"application/json\"\n                },\n                \"body\": JSON.stringify({\n                    'createAccountSuccess': false,\n                    'reason': 'The maximum spend field must be a number'\n                })\n            };\n        }\n        \n        maximumspend = parseFloat(maximumspend);\n        if (maximumspend <= 0) {\n            return {\n                \"statusCode\": 400,\n                \"isBase64Encoded\": false,\n                \"headers\": {\n                    \"Content-Type\": \"application/json\"\n                },\n                \"body\": JSON.stringify({\n                    'createAccountSuccess': false,\n                    'reason': 'The maximum spend field must be greater than zero'\n                })\n            };\n        }\n        if (maximumspend > parseFloat(process.env.MAXIMUM_ACCOUNT_SPEND)) {\n            return {\n                \"statusCode\": 400,\n                \"isBase64Encoded\": false,\n                \"headers\": {\n                    \"Content-Type\": \"application/json\"\n                },\n                \"body\": JSON.stringify({\n                    'createAccountSuccess': false,\n                    'reason': 'The maximum spend field must not be greater than ' + process.env.MAXIMUM_ACCOUNT_SPEND\n                })\n            };\n        }\n    }\n\n    let accountid = null;\n    let provisionaccountfromproductop = null;\n    if (process.env.CONTROL_TOWER_MODE == \"true\") {\n        let productslist = await servicecatalog.searchProductsAsAdmin({\n            Filters: {\n                FullTextSearch: ['AWS Control Tower Account Factory']\n            }\n        }).promise();\n\n        if (productslist.ProductViewDetails.length != 1) {\n            return {\n                \"statusCode\": 503,\n                \"isBase64Encoded\": false,\n                \"headers\": {\n                    \"Content-Type\": \"application/json\"\n                },\n                \"body\": JSON.stringify({\n                    'createAccountSuccess': false,\n                    'reason': 'Could not find Account Factory product'\n                })\n            };\n        }\n        \n        let portfoliolist = await servicecatalog.listPortfoliosForProduct({\n            ProductId: productslist.ProductViewDetails[0].ProductViewSummary.ProductId\n        }).promise();\n\n        for (let portfolio of portfoliolist.PortfolioDetails) {\n            if (portfolio.DisplayName == \"AWS Control Tower Account Factory Portfolio\") {\n                await servicecatalog.associatePrincipalWithPortfolio({\n                    PortfolioId: portfolio.Id,\n                    PrincipalType: 'IAM',\n                    PrincipalARN: process.env.ROLE\n                }).promise().then(async () => {\n                    await new Promise((resolve) => {setTimeout(resolve, 2000)}); // eventual consistency issues\n                }).catch(err => {});\n            }\n        }\n\n        let artifactlist = await servicecatalog.listProvisioningArtifacts({\n            ProductId: productslist.ProductViewDetails[0].ProductViewSummary.ProductId\n        }).promise();\n\n        let pathlist = await servicecatalog.listLaunchPaths({\n            ProductId: productslist.ProductViewDetails[0].ProductViewSummary.ProductId\n        }).promise();\n\n        provisionaccountfromproductop = await servicecatalog.provisionProduct({\n            PathId: pathlist.LaunchPathSummaries[0].Id,\n            ProductId: productslist.ProductViewDetails[0].ProductViewSummary.ProductId,\n            ProvisionToken: Math.random().toString().substr(2),\n            ProvisionedProductName: \"account-\" + dateFormat(new Date(), \"yyyy-mm-dd-HH-MM-ss-\") + Math.random().toString().substr(2,8),\n            ProvisioningArtifactId: artifactlist.ProvisioningArtifactDetails.pop().Id,\n            ProvisioningParameters: [\n                {\n                    Key: 'SSOUserEmail',\n                    Value: user.email\n                },\n                {\n                    Key: 'AccountEmail',\n                    Value: accountemail\n                },\n                {\n                    Key: 'SSOUserFirstName',\n                    Value: user.name.split(\" \")[0]\n                },\n                {\n                    Key: 'SSOUserLastName',\n                    Value: user.name.split(\" \").pop()\n                },\n                {\n                    Key: 'ManagedOrganizationalUnit',\n                    Value: 'Custom'\n                },\n                {\n                    Key: 'AccountName',\n                    Value: accountname\n                },\n            ]\n        }).promise().catch(err => {\n            LOG.debug(err);\n        });\n\n        let accountsdata = [];\n        let accounts = [];\n\n        while (!accountid) {\n            await new Promise((resolve) => {setTimeout(resolve, 2000)});\n\n            accountsdata = await retryWrapper(organizations, 'listAccounts', {\n                // no params\n            });\n            \n            accounts = accountsdata.Accounts;\n            \n            while (accountsdata.NextToken) {\n                accountsdata = await retryWrapper(organizations, 'listAccounts', {\n                    NextToken: data.NextToken\n                });\n                accounts = accounts.concat(accountsdata.Accounts);\n            }\n            for (let account of accounts) {\n                if (account.Email == accountemail) {\n                    accountid = account.Id;\n                }\n            }\n        }\n    } else {\n        let createaccountop = await retryWrapper(organizations, 'createAccount', {\n            AccountName: accountname, \n            Email: accountemail,\n            IamUserAccessToBilling: 'ALLOW',\n            RoleName: 'OrganizationAccountAccessRole'\n        });\n\n        LOG.debug(\"Created account, waiting for state\");\n\n        while (createaccountop.CreateAccountStatus.State == \"IN_PROGRESS\") {\n            LOG.debug(\"Account creation still in progress...\");\n            await new Promise((resolve) => {setTimeout(resolve, 2000)});\n\n            createaccountop = await retryWrapper(organizations, 'describeCreateAccountStatus', {\n                CreateAccountRequestId: createaccountop.CreateAccountStatus.Id\n            });\n        }\n\n        if (createaccountop.CreateAccountStatus.State != \"SUCCEEDED\") {\n            LOG.debug(\"Account creation failure\");\n            LOG.debug(createaccountop);\n\n            let reason = 'The account could not be created for an unknown reason';\n            if (createaccountop.CreateAccountStatus.FailureReason == \"ACCOUNT_LIMIT_EXCEEDED\") {\n                reason = 'The account could not be created because the Organizational limit has been exceeded';\n            } else if (createaccountop.CreateAccountStatus.FailureReason == \"EMAIL_ALREADY_EXISTS\") {\n                reason = 'The account could not be created as the email address already exists';\n            } else if (createaccountop.CreateAccountStatus.FailureReason == \"INVALID_EMAIL\") {\n                reason = 'The account could not be created due to an invalid email address';\n            } else if (createaccountop.CreateAccountStatus.FailureReason == \"CONCURRENT_ACCOUNT_MODIFICATION\") {\n                reason = 'The account could not be created due to a conflicting operation';\n            } else if (createaccountop.CreateAccountStatus.FailureReason == \"INTERNAL_FAILURE\") {\n                reason = 'The account could not be created due to an internal failure in the Organizations service';\n            }\n\n            return {\n                \"statusCode\": 503,\n                \"isBase64Encoded\": false,\n                \"headers\": {\n                    \"Content-Type\": \"application/json\"\n                },\n                \"body\": JSON.stringify({\n                    'createAccountSuccess': false,\n                    'reason': reason\n                })\n            };\n        }\n\n        accountid = createaccountop.CreateAccountStatus.AccountId;\n    }\n\n    let tags = [\n        {\n            Key: \"AccountOwnerGUID\",\n            Value: user.guid\n        },\n        {\n            Key: \"SSOCreationComplete\",\n            Value: \"false\"\n        }\n    ];\n\n    if (process.env.CONTROL_TOWER_MODE == \"true\") {\n        tags.push({\n            Key: \"ServiceCatalogProvisionedProductId\",\n            Value: provisionaccountfromproductop.RecordDetail.ProvisionedProductId\n        });\n    }\n\n    if (notes.length > 0) {\n        tags.push({\n            Key: \"Notes\",\n            Value: notes\n        });\n    }\n    if (form['shareaccount'] && form['shareaccount'] == \"on\") {\n        tags.push({\n            Key: \"SharedWithOrg\",\n            Value: \"true\"\n        });\n    }\n    if (process.env.ROOT_EMAILS_TO_USER == \"true\") {\n        tags.push({\n            Key: \"AccountEmailForwardingAddress\",\n            Value: user.email\n        });\n    }\n    if (maximumspend) {\n        tags.push({\n            Key: \"BudgetThresholdBeforeDeletion\",\n            Value: maximumspend.toString()\n        });\n    }\n\n    if (process.env.AUTO_UNSUB_MARKETING == \"true\") {\n        let unsubbody = `FirstName=&LastName=&Email=${encodeURIComponent(accountemail)}&Company=&Phone=&Country=&preferenceCenterCategory=no&preferenceCenterGettingStarted=no&preferenceCenterOnlineInPersonEvents=no&preferenceCenterMonthlyAWSNewsletter=no&preferenceCenterTrainingandBestPracticeContent=no&preferenceCenterProductandServiceAnnoucements=no&preferenceCenterSurveys=no&PreferenceCenter_AWS_Partner_Events_Co__c=no&preferenceCenterOtherAWSCommunications=no&PreferenceCenter_Language_Preference__c=&Title=&Job_Role__c=&Industry=&Level_of_AWS_Usage__c=&LDR_Solution_Area__c=&Unsubscribed=yes&UnsubscribedReason=&unsubscribedReasonOther=&useCaseMultiSelect=&zOPFormValidationBotVerification=&Website_Referral_Code__c=&zOPURLTrackingTRKCampaign=&zOPEmailValidationHygiene=validate&formid=34006&formVid=34006`;\n\n        await rp({ uri: 'https://pages.awscloud.com/index.php/leadCapture/save2', method: 'POST', body: unsubbody}).catch(err => {\n            LOG.warn(\"Failed to unsubscribe from marketing communications\");\n            LOG.warn(err);\n        });\n    }\n\n    await retryWrapper(organizations, 'tagResource', {\n        ResourceId: accountid,\n        Tags: tags\n    });\n\n    return {\n        \"statusCode\": 200,\n        \"isBase64Encoded\": false,\n        \"headers\": {\n            \"Content-Type\": \"application/json\"\n        },\n        \"body\": JSON.stringify({\n            'createAccountSuccess': true\n        })\n    };\n}\n\nfunction wrapHTML(user) {\n    return `<!doctype html>\n    <html lang=\"en\">\n      <head>\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n        <meta name=\"description\" content=\"\">\n        <title>${user.ssoprops.SSOManagerAppName}</title>\n\n        <link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\" integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\" crossorigin=\"anonymous\">\n        <style>\n        .fa-trash-alt:hover:before {\n            color: #f64f5f !important\n        }\n        </style>\n\n        <script src=\"https://kit.fontawesome.com/a9a4873efc.js\" crossorigin=\"anonymous\"></script>\n      </head>\n      <body class=\"bg-light\">\n        <div class=\"container\">\n        <div class=\"row\">\n        <div class=\"col-md-12\">\n        <p class=\"float-right mt-4 text-muted\">${user.name} (${user.email})&nbsp;&nbsp;|&nbsp;&nbsp;<a href=\"${user.ssoprops.SignOutURL}\">Back to SSO</a></p>\n        </div>\n        </div>\n\n        <div id=\"alerts\"></div>\n      \n        <div class=\"py-5 text-center\" style=\"padding-top: 1rem!important;\">\n        <svg class=\"d-block mx-auto mb-4\" height=\"72\" viewBox=\"0 0 64 64\" width=\"72\" xmlns=\"http://www.w3.org/2000/svg\"><g id=\"AccMgrLogo\" data-name=\"AccMgrLogo\"><path d=\"m53.54 41.34a8.047 8.047 0 0 0 -4.54-4.76v-25.58h-44a2.006 2.006 0 0 0 -2 2v6h40v17.59c-.23.09-.46.2-.68.31a11.984 11.984 0 0 0 -22.15 4.14 10 10 0 0 0 .83 19.96h30a9.993 9.993 0 0 0 2.54-19.66z\" fill=\"#bddbff\"/><g fill=\"#57a4ff\"><path d=\"m6 14h2v2h-2z\"/><path d=\"m10 14h2v2h-2z\"/><path d=\"m14 14h2v2h-2z\"/><path d=\"m38 14h2v2h-2z\"/><path d=\"m12 6h2v2h-2z\"/><path d=\"m16 6h2v2h-2z\"/><path d=\"m20 6h2v2h-2z\"/><path d=\"m44 6h2v2h-2z\"/><path d=\"m54.29 40.51a8.985 8.985 0 0 0 -4.29-4.55v-30.96a3.009 3.009 0 0 0 -3-3h-36a3.009 3.009 0 0 0 -3 3v5h-3a3.009 3.009 0 0 0 -3 3v30a3.009 3.009 0 0 0 3 3h6.23a10.874 10.874 0 0 0 -1.23 5 11.007 11.007 0 0 0 11 11h30a11 11 0 0 0 3.29-21.49zm-44.29-35.51a1 1 0 0 1 1-1h36a1 1 0 0 1 1 1v5h-38zm33.82 7h4.18v23.25a8.454 8.454 0 0 0 -4-.02v-22.23a3 3 0 0 0 -.18-1zm-39.82 1a1 1 0 0 1 1-1h36a1 1 0 0 1 1 1v5h-38zm1 31a1 1 0 0 1 -1-1v-23h38v14.75a12.956 12.956 0 0 0 -22.67 5.38 11.047 11.047 0 0 0 -6.78 3.87zm46 16h-30a9 9 0 0 1 -.74-17.96 1 1 0 0 0 .9-.84 10.982 10.982 0 0 1 20.3-3.79 1 1 0 0 0 1.32.38 6.846 6.846 0 0 1 3.22-.79 7 7 0 0 1 6.59 4.67.993.993 0 0 0 .69.63 9 9 0 0 1 -2.28 17.7z\"/><path d=\"m52.776 44.239-.506 1.936a4.994 4.994 0 0 1 -1.27 9.825v2a6.994 6.994 0 0 0 1.776-13.761z\"/><path d=\"m16 51a5.018 5.018 0 0 1 4.582-4.974l-.163-1.994a7 7 0 0 0 .581 13.968v-2a5.006 5.006 0 0 1 -5-5z\"/><path d=\"m23 56h4v2h-4z\"/></g></g></svg>\n        <h2>${user.ssoprops.SSOManagerAppName}</h2>\n        <p class=\"lead\">Below you can manage the AWS accounts that you have access to.</p>\n      </div>\n    \n      <div class=\"row\">\n        <div class=\"col-md-6 order-md-1 mb-6\">\n          <h4 class=\"d-flex justify-content-between align-items-center mb-3\">\n            <span>Your accounts</span>\n            <span id=\"accounts-count\" class=\"badge badge-secondary badge-pill\">-</span>\n          </h4>\n          <ul id=\"accounts-list\" class=\"list-group mb-3\">\n          </ul>\n        </div>\n        <div class=\"col-md-1 order-md-2\"></div>\n        <div class=\"col-md-5 order-md-3\">\n          <h4 class=\"mb-3\">Create account</h4>\n          <form id=\"create-account-form\" class=\"needs-validation\" novalidate>\n            <input id=\"SAMLResponse\" type=\"hidden\" name=\"SAMLResponse\" value=\"${user.samlresponse}\">\n\n            <div class=\"mb-3\">\n                <label for=\"emailprefix\">E-mail Prefix</label>\n                <div class=\"input-group\">\n                    <input type=\"text\" class=\"form-control\" id=\"emailprefix\" name=\"emailprefix\" placeholder=\"some-identifier\" required>\n                    <div class=\"input-group-prepend\">\n                        <span class=\"input-group-text\">@${process.env.DOMAIN_NAME}</span>\n                    </div>\n                    <div class=\"invalid-feedback\" style=\"width: 100%;\">\n                    An e-mail prefix is required.\n                    </div>\n                </div>\n            </div>\n    \n            <div class=\"mb-3\">\n                <label for=\"accountname\">Account Name</label>\n                <input type=\"text\" class=\"form-control\" id=\"accountname\" name=\"accountname\" placeholder=\"My Account\" required>\n                <div class=\"invalid-feedback\">\n                    An account name is required.\n                </div>\n            </div>\n            ${(process.env.MAXIMUM_ACCOUNT_SPEND == \"0\") ? '' : `\n\n            <div class=\"mb-3\">\n                <label for=\"accountname\">Maximum Monthly Spend (USD)</label>\n                <div class=\"input-group\">\n                    <div class=\"input-group-prepend\">\n                        <span class=\"input-group-text\">$</span>\n                    </div>\n                    <input type=\"text\" class=\"form-control\" id=\"maximumspend\" name=\"maximumspend\" value=\"${process.env.MAXIMUM_ACCOUNT_SPEND}\" aria-describedby=\"maximumspendhelp\" required>\n                    <small id=\"maximumspendhelp\" class=\"form-text text-muted\">\n                        Account will be automatically deleted when this threshold is reached.\n                    </small>\n                    <div class=\"invalid-feedback\" style=\"width: 100%;\">\n                    A maximum spend is required.\n                    </div>\n                </div>\n            </div>\n            `}\n            \n            <div class=\"mb-3\">\n                <label for=\"notes\">Notes <span class=\"text-muted\">(Optional)</span></label>\n                <input type=\"text\" class=\"form-control\" id=\"notes\" name=\"notes\">\n            </div>\n    \n            <hr class=\"mb-4\">\n\n            <div class=\"custom-control custom-checkbox\">\n              <input type=\"checkbox\" class=\"custom-control-input\" id=\"shareaccount\" name=\"shareaccount\">\n              <label class=\"custom-control-label\" for=\"shareaccount\">This account can be accessed by everyone in my organization</label>\n            </div>\n\n            <hr class=\"mb-4\">\n\n            <button id=\"create-account-submit-button\" class=\"btn btn-primary btn-lg btn-block\" type=\"submit\">Create Account</button>\n          </form>\n        </div>\n      </div>\n\n      <div class=\"modal fade\" id=\"delete-account-modal\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n            <div class=\"modal-body\">\n                <br />\n                <p>Are you sure you want to delete <strong id=\"delete-account-confirmation-text\"></strong>?</p>\n            </div>\n            <div class=\"modal-footer\">\n                <button type=\"button\" class=\"btn btn-secondary\" data-dismiss=\"modal\">Close</button>\n                <button id=\"delete-account-confirmation-button\" data-accountid=\"\" type=\"button\" class=\"btn btn-danger\">Delete Account</button>\n            </div>\n            </div>\n        </div>\n      </div>\n    \n      <footer class=\"my-5 pt-5 text-muted text-center text-small\">\n        <p class=\"mb-1\">For support, contact your administrator at <a href=\"mailto:${process.env.MASTER_EMAIL}\">${process.env.MASTER_EMAIL}</a></p>\n      </footer>\n    </div>\n    <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js\" crossorigin=\"anonymous\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js\" integrity=\"sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo\" crossorigin=\"anonymous\"></script>\n    <script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js\" integrity=\"sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6\" crossorigin=\"anonymous\"></script>\n    <script>\n        function refreshAccounts() {\n            $.ajax({\n                type: 'POST',\n                url: '/accounts',\n                data: 'SAMLResponse=' + $('#SAMLResponse').val(),\n                success: function(response) {\n                    $('#accounts-list').html('');\n                    $('#accounts-count').html(response.accounts.length);\n                    for (const account of response.accounts) {\n                        $('#accounts-list').append(\\`\n                            <li class=\"list-group-item d-flex justify-content-between lh-condensed\">\n                            <div>\n                                <h6 class=\"my-0\">\\${account.Name}\\${account.IsShared ? '&nbsp;&nbsp;<span class=\"badge badge-dark\">SHARED</span>' : ''}\\${account.IsDeleting ? '&nbsp;&nbsp;<span class=\"badge badge-warning\">DELETING</span>' : ''}\\${account.IsCreating ? '&nbsp;&nbsp;<span class=\"badge badge-success\">CREATING</span>' : ''}</h6>\n                                <small class=\"text-muted\">Account ID: \\${account.Id}</small><br />\n                                <small class=\"text-muted\">Account E-mail: \\${account.Email}</small><br />\n                                <small class=\"text-muted\">Notes: \\${account.Notes || ''}</small>\n                            </div>\n                            <span>\\${((account.IsShared && !account.IsOwner) || account.IsDeleting || account.IsCreating) ? '' : \\`<i class=\"fas fa-trash-alt text-danger\" data-toggle=\"modal\" data-target=\"#delete-account-modal\" data-accountname=\"\\${account.Name}\" data-accountid=\"\\${account.Id}\"></i>\\`}</span>\n                            </li>\n                        \\`);\n                    }\n                },\n                error: function(response) {\n                    if ($('#alerts').html() == \"\") {\n                        $('#alerts').append(\\`\n                            <div class=\"alert alert-danger alert-dismissible fade show\" role=\"alert\">\n                            <strong>Account List Failure</strong> The list of accounts could not be loaded for an unknown reason\n                            <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\">\n                                <span aria-hidden=\"true\">&times;</span>\n                            </button>\n                            </div>\n                        \\`);\n\n                        window.scrollTo(0, 0);\n                    }\n                },\n            });\n        }\n\n        function deleteAccount(accountid) {\n            $.ajax({\n                type: 'POST',\n                url: '/deleteaccount',\n                data: 'accountid=' + accountid.trim() + '&SAMLResponse=' + $('#SAMLResponse').val(),\n                success: function(response) {\n                    $('#alerts').append(\\`\n                        <div class=\"alert alert-success alert-dismissible fade show\" role=\"alert\">\n                        <strong>Account Deletion Requested</strong> Your AWS account deletion request has been successfully processed. This will occur within the next 5 minutes.\n                        <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\">\n                            <span aria-hidden=\"true\">&times;</span>\n                        </button>\n                        </div>\n                    \\`);\n\n                    $('#delete-account-modal').modal('hide');\n\n                    refreshAccounts();\n\n                    window.scrollTo(0, 0);\n                },\n                error: function(response) {\n                    $('#alerts').append(\\`\n                        <div class=\"alert alert-danger alert-dismissible fade show\" role=\"alert\">\n                        <strong>Account Creation Failure</strong> The account could not be deleted for an unknown reason\n                        <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\">\n                            <span aria-hidden=\"true\">&times;</span>\n                        </button>\n                        </div>\n                    \\`);\n\n                    $('#delete-account-modal').modal('hide');\n\n                    window.scrollTo(0, 0);\n                },\n            });\n        }\n\n        $('#create-account-form').submit(e => {\n            e.preventDefault();\n\n            $('#create-account-submit-button').attr('disabled', 'disabled');\n\n            $.ajax({\n                type: 'POST',\n                url: '/createaccount',\n                data: $('#create-account-form').serialize(),\n                success: function(response) {\n                    $('#alerts').append(\\`\n                        <div class=\"alert alert-success alert-dismissible fade show\" role=\"alert\">\n                        <strong>Account Created</strong> Your AWS account has been created successfully. It will be available to use via SSO in a few minutes.\n                        <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\">\n                            <span aria-hidden=\"true\">&times;</span>\n                        </button>\n                        </div>\n                    \\`);\n\n                    // reset form\n                    $('#create-account-form').find('input[type=\"text\"]').val('');\n                    $('#create-account-form').find('input[type=\"checkbox\"]').prop('checked', false);\n                    $('#create-account-submit-button').removeAttr('disabled');\n\n                    refreshAccounts();\n\n                    window.scrollTo(0, 0);\n                },\n                error: function(response) {\n                    var reason = \"The account could not be created for an unknown reason\";\n                    if (response.responseJSON && response.responseJSON.reason) {\n                        reason = response.responseJSON.reason;\n                    }\n\n                    $('#alerts').append(\\`\n                        <div class=\"alert alert-danger alert-dismissible fade show\" role=\"alert\">\n                        <strong>Account Creation Failure</strong> \\${reason}\n                        <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\">\n                            <span aria-hidden=\"true\">&times;</span>\n                        </button>\n                        </div>\n                    \\`);\n\n                    $('#create-account-submit-button').removeAttr('disabled');\n\n                    window.scrollTo(0, 0);\n                },\n            });\n        });\n\n        $(document).ready(function() {\n            $('#delete-account-modal').on('show.bs.modal', function (event) {\n                var button = $(event.relatedTarget);\n                var accountid = button.data('accountid');\n                var accountname = button.data('accountname');\n                \n                $('#delete-account-confirmation-text').html(accountname + \" (\" + accountid + \")\");\n                $('#delete-account-confirmation-button').attr('data-accountid', accountid);\n                $('#delete-account-confirmation-button').removeAttr('disabled');\n            });\n\n            $('#delete-account-confirmation-button').click(function (event) {\n                $('#delete-account-confirmation-button').attr('disabled', 'disabled');\n                deleteAccount($('#delete-account-confirmation-button').attr('data-accountid'));\n            });\n\n            refreshAccounts();\n        });\n\n        setInterval(refreshAccounts, 10000);\n    </script>\n    </body>\n    </html>\n    `;\n}\n\nexports.handler = async (event, context) => {\n    let result = null;\n    let browser = null;\n\n    LOG.debug(event);\n\n    if (event.source && event.source == \"aws.organizations\" && event.detail.eventName == \"TagResource\") {\n        isdeletable = false;\n        accountowner = null;\n        isshared = false;\n        budgetthresholdbeforedeletion = null;\n        event.detail.requestParameters.tags.forEach(tag => {\n            if (tag.key.toLowerCase() == \"delete\" && tag.value.toLowerCase() == \"true\") {\n                isdeletable = true;\n            }\n            if (tag.key.toLowerCase() == \"accountownerguid\") {\n                accountowner = tag.value;\n            }\n            if (tag.key.toLowerCase() == \"budgetthresholdbeforedeletion\") {\n                budgetthresholdbeforedeletion = tag.value;\n            }\n            if (tag.key.toLowerCase() == \"sharedwithorg\" && tag.value.toLowerCase() == \"true\") {\n                isshared = true;\n            }\n        });\n\n        if (isdeletable && process.env.DELETION_FUNCTIONALITY_ENABLED == \"true\") {\n            let data = await retryWrapper(organizations, 'describeAccount', {\n                AccountId: event.detail.requestParameters.resourceId\n            });\n\n            browser = await puppeteer.launch({\n                args: chromium.args,\n                defaultViewport: chromium.defaultViewport,\n                executablePath: await chromium.executablePath,\n                headless: chromium.headless,\n            });\n    \n            let page = await browser.newPage();\n    \n            await triggerReset(page, {\n                'email': data.Account.Email\n            });\n        }\n\n        if (accountowner && process.env.CREATION_FUNCTIONALITY_ENABLED == \"true\") {\n            browser = await puppeteer.launch({\n                args: chromium.args,\n                defaultViewport: chromium.defaultViewport,\n                executablePath: await chromium.executablePath,\n                headless: chromium.headless,\n            });\n    \n            let page = await browser.newPage();\n\n            await login(page);\n\n            if (process.env.DENY_SUBSCRIPTION_CALLS) {\n                await addSubscriptionsSCP({\n                    'accountid': event.detail.requestParameters.resourceId\n                });\n            }\n\n            if (budgetthresholdbeforedeletion) {\n                await addBillingMonitor(page, {\n                    'accountid': event.detail.requestParameters.resourceId,\n                    'budgetthresholdbeforedeletion': budgetthresholdbeforedeletion\n                });\n            }\n    \n            await setSSOOwner(page, {\n                'accountowner': accountowner,\n                'accountid': event.detail.requestParameters.resourceId,\n                'isshared': isshared\n            });\n        }\n    } else if (event.email) {\n        browser = await puppeteer.launch({\n            args: chromium.args,\n            defaultViewport: chromium.defaultViewport,\n            executablePath: await chromium.executablePath,\n            headless: chromium.headless,\n        });\n\n        let page = await browser.newPage();\n\n        await triggerReset(page, event);\n    } else if (event.action == \"removeAccountFromOrg\") {\n        let removed = await removeAccountFromOrg(event.account);\n\n        if (removed) {\n            let targetsresponse = await eventbridge.listTargetsByRule({\n                Rule: event.ruleName\n            }).promise();\n\n            for (const target of targetsresponse.Targets) {\n                await eventbridge.removeTargets({\n                    Rule: event.ruleName,\n                    Ids: [target.Id]\n                }).promise();\n            }\n\n            await eventbridge.deleteRule({\n                Name: event.ruleName\n            }).promise();\n\n            LOG.info(\"Successfully removed rule\");\n        }\n    } else if (event.Records && event.Records[0] && event.Records[0].s3 && event.Records[0].s3.bucket) {\n        browser = await puppeteer.launch({\n            args: chromium.args,\n            defaultViewport: chromium.defaultViewport,\n            executablePath: await chromium.executablePath,\n            headless: chromium.headless,\n        });\n\n        let page = await browser.newPage();\n\n        await handleEmailInbound(page, event);\n    } else if (event.Records && event.Records[0] && event.Records[0].Sns) {\n        await processSnsDeleteAccount(event);\n    } else if (event.Name && event.Name == \"ContactFlowEvent\") {\n        let connectssmparameter = await ssm.getParameter({\n            Name: process.env.CONNECT_SSM_PARAMETER\n        }).promise();\n\n        let variables = JSON.parse(connectssmparameter['Parameter']['Value']);\n\n        return {\n            \"prompt1\": variables['PROMPT_' + variables['CODE'][0]],\n            \"prompt2\": variables['PROMPT_' + variables['CODE'][1]],\n            \"prompt3\": variables['PROMPT_' + variables['CODE'][2]],\n            \"prompt4\": variables['PROMPT_' + variables['CODE'][3]]\n        }\n    } else if (event.ResourceType == \"Custom::ConnectSetup\") {\n        let domain = event.StackId.split(\"-\").pop();\n\n        browser = await puppeteer.launch({\n            args: chromium.args,\n            defaultViewport: chromium.defaultViewport,\n            executablePath: await chromium.executablePath,\n            headless: chromium.headless,\n        });\n\n        let page = await browser.newPage();\n\n        try {\n            await login(page);\n\n            if (event.RequestType == \"Create\") {\n                await ses.setActiveReceiptRuleSet({\n                    RuleSetName: \"account-controller\"\n                }).promise();\n\n                await createinstance(page, {\n                    'Domain': domain\n                });\n                await page.waitFor(5000);\n                await open(page, {\n                    'Domain': domain\n                });\n                let hostx = new url.URL(await page.url()).host;\n                while (hostx.indexOf(domain) == -1) {\n                    await page.waitFor(20000);\n                    await open(page, {\n                        'Domain': domain\n                    });\n                    hostx = new url.URL(await page.url()).host;\n                }\n                let prompts = await uploadprompts(page, {\n                    'Domain': domain\n                });\n                await createflow(page, {\n                    'Domain': domain\n                }, prompts);\n                let number = await claimnumber(page, {\n                    'Domain': domain\n                });\n                LOG.info(\"Registered phone number: \" + number['PhoneNumber']);\n                \n                let variables = {};\n\n                ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].forEach(num => {\n                    variables['PROMPT_' + num] = prompts[num + '.wav'];\n                });\n                variables['PHONE_NUMBER'] = number['PhoneNumber'].replace(/[ -]/g, \"\")\n    \n                await ssm.putParameter({\n                    Name: process.env.CONNECT_SSM_PARAMETER,\n                    Type: \"String\",\n                    Value: JSON.stringify(variables),\n                    Overwrite: true\n                }).promise();\n            } else if (event.RequestType == \"Delete\") {\n                await ses.setActiveReceiptRuleSet({\n                    RuleSetName: \"default-rule-set\"\n                }).promise();\n\n                await ses.deleteReceiptRuleSet({\n                    RuleSetName: \"account-controller\"\n                }).promise();\n\n                await deleteinstance(page, {\n                    'Domain': domain\n                });\n            }\n\n            await sendcfnresponse(event, context, \"SUCCESS\", {\n                'Domain': domain\n            }, domain);\n        } catch(error) {\n            await sendcfnresponse(event, context, \"FAILED\", {});\n\n            await debugScreenshot(page);\n\n            throw error;\n        }\n    } else if (event.ResourceType == \"Custom::SSOSetup\") {\n        browser = await puppeteer.launch({\n            args: chromium.args,\n            defaultViewport: chromium.defaultViewport,\n            executablePath: await chromium.executablePath,\n            headless: chromium.headless,\n        });\n\n        let page = await browser.newPage();\n\n        try {\n            await login(page);\n\n            if (event.RequestType == \"Create\") {\n                await createssoapp(page, {\n                    'SSOManagerAppName': event.ResourceProperties.SSOManagerAppName,\n                    'APIGatewayEndpoint': event.ResourceProperties.APIGatewayEndpoint\n                });\n            } else if (event.RequestType == \"Delete\") {\n                await deletessoapp(page, {\n                    'SSOManagerAppName': event.ResourceProperties.SSOManagerAppName,\n                    'APIGatewayEndpoint': event.ResourceProperties.APIGatewayEndpoint\n                });\n            }\n\n            await sendcfnresponse(event, context, \"SUCCESS\", {\n                \"SSOManagerAppName\": event.ResourceProperties.SSOManagerAppName,\n                'APIGatewayEndpoint': event.ResourceProperties.APIGatewayEndpoint\n            }, \"SSOManager\");\n        } catch(error) {\n            await sendcfnresponse(event, context, \"FAILED\", {});\n\n            await debugScreenshot(page);\n\n            throw error;\n        }\n    } else if (event.routeKey == \"GET /\") {\n        let ssoparamresponse = await ssm.getParameter({\n            Name: process.env.SSO_SSM_PARAMETER\n        }).promise();\n        let ssoproperties = JSON.parse(ssoparamresponse['Parameter']['Value']);\n        \n        return {\n            \"statusCode\": 302,\n            \"headers\": {\n                \"Location\": ssoproperties['SignOutURL']\n            }\n        };\n    } else if (event.routeKey == \"POST /\") {\n        try {\n            let resp = await handleSAMLResponse(event);\n\n            return resp;\n        } catch(err) {\n            LOG.error(err);\n        }\n\n        return {\n            \"statusCode\": 500,\n            \"isBase64Encoded\": false,\n            \"headers\": {\n                \"Content-Type\": \"index/html\"\n            },\n            \"body\": \"\"\n        };\n    } else if (event.routeKey == \"POST /accounts\") {\n        try {\n            let resp = await handleGetAccounts(event);\n            return resp;\n        } catch(err) {\n            LOG.error(err);\n        }\n\n        return {\n            \"statusCode\": 500,\n            \"isBase64Encoded\": false,\n            \"headers\": {\n                \"Content-Type\": \"application/json\"\n            },\n            \"body\": \"\"\n        };\n    } else if (event.routeKey == \"POST /createaccount\") {\n        try {\n            let resp = await handleCreateAccountRequest(event);\n            return resp;\n        } catch(err) {\n            LOG.error(err);\n        }\n\n        return {\n            \"statusCode\": 500,\n            \"isBase64Encoded\": false,\n            \"headers\": {\n                \"Content-Type\": \"application/json\"\n            },\n            \"body\": \"\"\n        };\n    } else if (event.routeKey == \"POST /deleteaccount\") {\n        try {\n            let resp = await handleDeleteAccountRequest(event);\n            return resp;\n        } catch(err) {\n            LOG.error(err);\n        }\n        \n        return {\n            \"statusCode\": 500,\n            \"isBase64Encoded\": false,\n            \"headers\": {\n                \"Content-Type\": \"application/json\"\n            },\n            \"body\": \"\"\n        };\n    } else {\n        return context.succeed();\n    }\n};\n\n"
  },
  {
    "path": "lambda/package.json",
    "content": "{\n  \"name\": \"aws-account-controller\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Manage the creation and deletion of sandbox-style accounts\",\n  \"main\": \"index.js\",\n  \"dependencies\": {\n    \"aws-sdk\": \"^2.814.0\",\n    \"chrome-aws-lambda\": \"^2.1.1\",\n    \"dateformat\": \"^3.0.3\",\n    \"https\": \"^1.0.0\",\n    \"internet-message\": \"github:iann0036/js-internet-message\",\n    \"mailparser-mit\": \"^1.0.0\",\n    \"puppeteer-core\": \"^2.1.1\",\n    \"request\": \"^2.88.0\",\n    \"request-promise\": \"^4.2.4\",\n    \"saml2-js\": \"^2.0.5\",\n    \"winston\": \"^3.2.1\"\n  },\n  \"devDependencies\": {},\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/iann0036/aws-account-controller.git\"\n  },\n  \"author\": \"Ian Mckay\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/iann0036/aws-account-controller/issues\"\n  },\n  \"homepage\": \"https://github.com/iann0036/aws-account-controller#readme\"\n}\n"
  },
  {
    "path": "template.yml",
    "content": "AWSTemplateFormatVersion: \"2010-09-09\"\n\nDescription: Account Controller Solution\n\nParameters:\n\n    ControlTowerMode:\n        Description: Provision accounts with Control Tower instead of directly with Organizations (requires Control Tower to be set up)\n        Type: String\n        Default: \"false\"\n        AllowedValues:\n          - \"true\"\n          - \"false\"\n\n    AccountCreationFunctionality:\n        Description: Manage SSO and other features required for account creation functionality\n        Type: String\n        Default: \"true\"\n        AllowedValues:\n          - \"true\"\n          - \"false\"\n\n    AccountDeletionFunctionality:\n        Description: Manage Connect and other features required for account deletion functionality\n        Type: String\n        Default: \"true\"\n        AllowedValues:\n          - \"true\"\n          - \"false\"\n\n    AutoUnsubMarketing:\n        Description: Automatically unsubscribe newly created accounts from all marketing material\n        Type: String\n        Default: \"true\"\n        AllowedValues:\n          - \"true\"\n          - \"false\"\n\n    MasterEmail:\n        Description: The email address which will receive all root account correspondence (this should NOT be an address of your master domain/subdomain)\n        Type: String\n        AllowedPattern: \"^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]+$\"\n\n    MaximumAccountSpend:\n        Description: The maximum configurable monthly spend, in USD, of created accounts before they are automatically deleted - or set to \"0\" to disable spend tracking\n        Type: String\n        Default: \"100\"\n        AllowedPattern: \"^[0-9]+(?:\\\\.[0-9]{2})?$\"\n\n    EmailSubjectCustomization:\n        Description: The format of the forwarded emails\n        Type: String\n        Default: \"{subject} | From: {from} | Acct ID: {accountid} | Acct Email: {accountemail}\"\n\n    RootEmailsToUser:\n        Description: If true, root e-mails will be sent to the user who created the account, otherwise they will be sent to the master e-mail address\n        Type: String\n        Default: \"true\"\n        AllowedValues:\n          - \"true\"\n          - \"false\"\n\n    DenySubscriptionCalls:\n        Description: Denies the capability to make subscription-based calls in new accounts, like reserved instances, via an SCP\n        Type: String\n        Default: \"true\"\n        AllowedValues:\n          - \"true\"\n          - \"false\"\n\n    DomainName:\n        Description: The domain name (or subdomain) which is used for all account root email addresses\n        Type: String\n\n    2CaptchaApiKey:\n        Description: The API Key for 2captcha.com\n        Type: String\n        Default: ''\n        NoEcho: true\n\n    S3Bucket:\n        Description: The name of the bucket that contains the Lambda source (leave blank to use latest)\n        Type: String\n        Default: ''\n    \n    S3Key:\n        Description: The key of the ZIP package within the bucket (leave blank to use latest)\n        Type: String\n        Default: ''\n\n    AutomationUsername:\n        Description: The username of an IAM user created to perform automated actions\n        Type: String\n        Default: AccountControllerAutomationUser\n    \n    LogLevel:\n        Description: The log level of the Lambda function\n        Type: String\n        Default: \"INFO\"\n        AllowedValues:\n          - \"DEBUG\"\n          - \"INFO\"\n          - \"WARN\"\n          - \"ERROR\"\n    \n    CCName:\n        Description: The full name of the credit card owner\n        Type: String\n    \n    CCNumber:\n        Description: The number of the credit card\n        Type: String\n        NoEcho: true\n    \n    CCMonth:\n        Description: The month of the credit card as a number (January = 1, December = 12)\n        Type: String\n        AllowedValues:\n          - \"1\"\n          - \"2\"\n          - \"3\"\n          - \"4\"\n          - \"5\"\n          - \"6\"\n          - \"7\"\n          - \"8\"\n          - \"9\"\n          - \"10\"\n          - \"11\"\n          - \"12\"\n    \n    CCYear:\n        Description: The full year of the credit card (e.g. 2020)\n        Type: String\n        AllowedValues:\n          - \"2020\"\n          - \"2021\"\n          - \"2022\"\n          - \"2023\"\n          - \"2024\"\n          - \"2025\"\n          - \"2026\"\n          - \"2027\"\n          - \"2028\"\n          - \"2029\"\n          - \"2030\"\n          - \"2031\"\n          - \"2032\"\n          - \"2033\"\n          - \"2034\"\n          - \"2035\"\n          - \"2036\"\n          - \"2037\"\n          - \"2038\"\n          - \"2039\"\n    \n    HostedZoneId:\n        Description: The ID of the hosted zone of the previous domain name (leave blank for this to be created for you)\n        Type: String\n        Default: ''\n\n    SSOManagerAppName:\n        Description: The name of the SSO application used to manage accounts\n        Type: String\n        Default: Account Manager\n\nMetadata: \n\n    AWS::CloudFormation::Interface: \n        ParameterGroups: \n          - Label: \n                default: \"Global Features\"\n            Parameters: \n              - AccountCreationFunctionality\n              - AccountDeletionFunctionality\n              - ControlTowerMode\n          - Label: \n                default: \"Email Configuration\"\n            Parameters: \n              - MasterEmail\n              - DomainName\n              - HostedZoneId\n              - EmailSubjectCustomization\n              - RootEmailsToUser\n              - AutoUnsubMarketing\n          - Label: \n                default: \"Billing Credit Card\"\n            Parameters: \n              - CCName\n              - CCNumber\n              - CCMonth\n              - CCYear\n          - Label: \n                default: \"SSO Settings\"\n            Parameters: \n              - SSOManagerAppName\n          - Label: \n                default: \"Other Settings\"\n            Parameters: \n              - 2CaptchaApiKey\n              - AutomationUsername\n              - MaximumAccountSpend\n              - DenySubscriptionCalls\n          - Label: \n                default: \"Lambda Function\"\n            Parameters: \n              - LogLevel\n              - S3Bucket\n              - S3Key\n        ParameterLabels: \n            MasterEmail: \n                default: \"Master Email Address\"\n            DomainName: \n                default: \"Master Domain Name\"\n            HostedZoneId: \n                default: \"Hosted Zone ID\"\n            CCName: \n                default: \"Credit Card Name\"\n            CCNumber: \n                default: \"Credit Card Number\"\n            CCMonth: \n                default: \"Credit Card Expiry Month\"\n            CCYear: \n                default: \"Credit Card Expiry Year\"\n            2CaptchaApiKey: \n                default: \"2Captcha API Key\"\n            S3Bucket: \n                default: \"S3 Bucket\"\n            S3Key: \n                default: \"S3 Key\"\n            LogLevel: \n                default: \"Log Level\"\n            EmailSubjectCustomization: \n                default: \"E-mail Subject Customization\"\n            AccountCreationFunctionality: \n                default: \"Enable Account Creation Functionality\"\n            AccountDeletionFunctionality: \n                default: \"Enable Account Deletion Functionality\"\n            SSOManagerAppName: \n                default: \"SSO Account Manager Application Name\"\n            AutomationUsername: \n                default: \"Automation IAM User Username\"\n            RootEmailsToUser:\n                default: \"Send Root E-mails to User\"\n            MaximumAccountSpend:\n                default: \"Maximum Monthly Spend Per Account\"\n            DenySubscriptionCalls:\n                default: \"Deny Subscription Calls\"\n            AutoUnsubMarketing:\n                default: \"Unsubscribe Marketing E-mails\"\n            ControlTowerMode:\n                default: \"Control Tower Mode\"\n\nConditions:\n\n    S3Defined: !Not [ !Equals [ '', !Ref S3Bucket ] ]\n    HostedZoneNotDefined: !Equals [ '', !Ref HostedZoneId ]\n    AccountCreationEnabled: !Equals [ 'true', !Ref AccountCreationFunctionality ]\n    AccountDeletionEnabled: !Equals [ 'true', !Ref AccountDeletionFunctionality ]\n    ControlTowerModeEnabled: !Equals [ 'true', !Ref ControlTowerMode ]\n\nMappings:\n    RegionMap:\n        us-east-1:\n            bucketname: ianmckay-us-east-1\n        us-east-2:\n            bucketname: ianmckay-us-east-2\n        us-west-1:\n            bucketname: ianmckay-us-west-1\n        us-west-2:\n            bucketname: ianmckay-us-west-2\n        ap-south-1:\n            bucketname: ianmckay-ap-south-1\n        ap-northeast-2:\n            bucketname: ianmckay-ap-northeast-2\n        ap-southeast-1:\n            bucketname: ianmckay-ap-southeast-1\n        ap-southeast-2:\n            bucketname: ianmckay-ap-southeast-2\n        ap-northeast-1:\n            bucketname: ianmckay-ap-northeast-1\n        ca-central-1:\n            bucketname: ianmckay-ca-central-1\n        eu-central-1:\n            bucketname: ianmckay-eu-central-1\n        eu-west-1:\n            bucketname: ianmckay-eu-west-1\n        eu-west-2:\n            bucketname: ianmckay-eu-west-2\n        eu-west-3:\n            bucketname: ianmckay-eu-west-3\n        eu-north-1:\n            bucketname: ianmckay-eu-north-1\n        sa-east-1:\n            bucketname: ianmckay-sa-east-1\n\nResources:\n\n    DebugBucket:\n        Type: AWS::S3::Bucket\n        Properties:\n            LifecycleConfiguration:\n                Rules:\n                  - NoncurrentVersionExpirationInDays: 14\n                    ExpirationInDays: 14\n                    Status: Enabled\n            BucketEncryption:\n                ServerSideEncryptionConfiguration:\n                  - ServerSideEncryptionByDefault:\n                        SSEAlgorithm: AES256\n            PublicAccessBlockConfiguration:\n                BlockPublicAcls: true\n                BlockPublicPolicy: true\n                IgnorePublicAcls: true\n                RestrictPublicBuckets: true\n\n    HostedZone:\n        Condition: HostedZoneNotDefined\n        Type: AWS::Route53::HostedZone\n        Properties:\n            Name: !Ref DomainName\n\n    MXRecord:\n        Type: AWS::Route53::RecordSet\n        Properties:\n            HostedZoneId: !If\n              - HostedZoneNotDefined\n              - !Ref HostedZone\n              - !Ref HostedZoneId\n            Name: !Sub '${DomainName}.'\n            Type: MX\n            TTL: '900'\n            ResourceRecords:\n              - !Sub '10 inbound-smtp.${AWS::Region}.amazonaws.com'\n    \n    OrgAccountTaggedRule:\n        Type: AWS::Events::Rule\n        Properties:\n            Description: Detect and begin processing accounts when tagged\n            EventPattern: |\n              {\n                \"source\": [\n                  \"aws.organizations\"\n                ],\n                \"detail-type\": [\n                  \"AWS API Call via CloudTrail\"\n                ],\n                \"detail\": {\n                  \"eventSource\": [\n                    \"organizations.amazonaws.com\"\n                  ],\n                  \"eventName\": [\n                    \"TagResource\"\n                  ]\n                }\n              }\n            State: ENABLED\n            Targets:\n              - Arn: !GetAtt LambdaFunction.Arn\n                Id: Action\n\n    LambdaAccountDeletionEventRulePermission:\n        Condition: AccountDeletionEnabled\n        Type: AWS::Lambda::Permission\n        Properties:\n            FunctionName: !Ref LambdaFunction\n            Action: lambda:InvokeFunction\n            Principal: events.amazonaws.com\n            SourceArn: !GetAtt OrgAccountTaggedRule.Arn\n\n    LambdaConnectPermission:\n        Condition: AccountDeletionEnabled\n        Type: AWS::Lambda::Permission\n        Properties:\n            FunctionName: !Ref LambdaFunction\n            Action: lambda:InvokeFunction\n            Principal: connect.amazonaws.com\n            SourceAccount: !Ref AWS::AccountId\n\n    LambdaAPIGatewayPermission:\n        Condition: AccountCreationEnabled\n        Type: AWS::Lambda::Permission\n        Properties:\n            FunctionName: !Ref LambdaFunction\n            Action: lambda:InvokeFunction\n            Principal: apigateway.amazonaws.com\n            SourceArn: !Sub \"arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${SAMLHttpApi}/*/*/*\"\n\n    LambdaDeleteAccountEventsPermission:\n        Condition: AccountDeletionEnabled\n        Type: AWS::Lambda::Permission\n        Properties:\n            FunctionName: !Ref LambdaFunction\n            Action: lambda:InvokeFunction\n            Principal: events.amazonaws.com\n            SourceArn: !Sub \"arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/ScheduledAccountDeletion-*\"\n\n    LambdaDeleteAccountSNSPermission:\n        Condition: AccountDeletionEnabled\n        Type: AWS::Lambda::Permission\n        Properties:\n            FunctionName: !Ref LambdaFunction\n            Action: lambda:InvokeFunction\n            Principal: sns.amazonaws.com\n            SourceArn: !Ref AccountDeletionSNSTopic\n\n    LambdaLogGroup:\n        Type: AWS::Logs::LogGroup\n        Properties:\n            LogGroupName: /aws/lambda/AccountAutomator\n            RetentionInDays: 14\n\n    LambdaFunction:\n        DependsOn:\n          - AutomationUser\n          - LambdaLogGroup\n        Type: AWS::Lambda::Function\n        Properties:\n            FunctionName: AccountAutomator\n            Code:\n                S3Bucket: !If\n                    - S3Defined\n                    - !Ref S3Bucket\n                    - Fn::FindInMap:\n                        - RegionMap\n                        - !Ref 'AWS::Region'\n                        - bucketname\n                S3Key: !If\n                    - S3Defined\n                    - !Ref S3Key\n                    - 'accountcontroller/app.zip'\n            Handler: index.handler\n            Role: !GetAtt LambdaExecutionRole.Arn\n            Environment:\n                Variables:\n                    DEBUG_BUCKET: !Ref DebugBucket\n                    CAPTCHA_KEY: !Ref 2CaptchaApiKey\n                    ACCOUNTID: !Ref AWS::AccountId\n                    MASTER_EMAIL: !Ref MasterEmail\n                    LOG_LEVEL: !Ref LogLevel\n                    EMAIL_SUBJECT: !Ref EmailSubjectCustomization\n                    SECRET_ARN: !Ref AutomationCredentials\n                    CONNECT_SSM_PARAMETER: !Ref ConnectProperties\n                    SSO_SSM_PARAMETER: !Ref SSOProperties\n                    DOMAIN_NAME: !Ref DomainName\n                    CREATION_FUNCTIONALITY_ENABLED: !Ref AccountCreationFunctionality\n                    DELETION_FUNCTIONALITY_ENABLED: !Ref AccountDeletionFunctionality\n                    ACCOUNT_DELETION_TOPIC: !If\n                      - AccountDeletionEnabled\n                      - !Ref AccountDeletionSNSTopic\n                      - !Ref AWS::NoValue\n                    ROOT_EMAILS_TO_USER: !Ref RootEmailsToUser\n                    MAXIMUM_ACCOUNT_SPEND: !Ref MaximumAccountSpend\n                    DENY_SUBSCRIPTION_CALLS: !Ref DenySubscriptionCalls\n                    AUTO_UNSUB_MARKETING: !Ref AutoUnsubMarketing\n                    CONTROL_TOWER_MODE: !Ref ControlTowerMode\n                    ROLE: !GetAtt LambdaExecutionRole.Arn\n            Runtime: nodejs12.x\n            MemorySize: 1024\n            Timeout: 900\n    \n    LambdaExecutionRole:\n        Type: AWS::IAM::Role\n        Properties:\n            AssumeRolePolicyDocument:\n                Version: '2012-10-17'\n                Statement:\n                  - Effect: Allow\n                    Principal:\n                        Service:\n                          - lambda.amazonaws.com\n                    Action:\n                      - sts:AssumeRole\n            Path: /\n            Policies:\n              - PolicyName: root\n                PolicyDocument:\n                    Version: '2012-10-17'\n                    Statement:\n                      - Effect: Allow\n                        Action:\n                          - logs:CreateLogGroup\n                          - logs:CreateLogStream\n                          - logs:PutLogEvents\n                        Resource: arn:aws:logs:*:*:*\n                      - Effect: Allow\n                        Action:\n                          - s3:GetObject\n                        Resource:\n                          - !Sub arn:aws:s3:::accountcontroller-email-processing-${AWS::Region}-${AWS::AccountId}/*\n                      - Effect: Allow\n                        Action:\n                          - s3:PutObject\n                        Resource:\n                          - !Sub arn:aws:s3:::${DebugBucket}/*\n                      - Effect: Allow\n                        Action:\n                          - organizations:DescribeAccount\n                        Resource:\n                          - !Sub arn:aws:organizations::${AWS::AccountId}:account/*\n                      - Effect: Allow\n                        Action:\n                          - sts:AssumeRole\n                        Resource:\n                          - arn:aws:iam::*:role/OrganizationAccountAccessRole\n                          - arn:aws:iam::*:role/AWSControlTowerExecution\n                      - Effect: Allow\n                        Action:\n                          - organizations:ListAccounts\n                          - organizations:ListTagsForResource\n                          - organizations:TagResource\n                          - organizations:CreateAccount\n                          - organizations:DescribeCreateAccountStatus\n                          - organizations:DescribeOrganization\n                          - organizations:ListPolicies\n                          - organizations:CreatePolicy\n                          - organizations:UpdatePolicy\n                          - organizations:AttachPolicy\n                          - organizations:RemoveAccountFromOrganization\n                          - ses:SendRawEmail\n                          - ses:SetActiveReceiptRuleSet\n                          - ses:DeleteReceiptRuleSet\n                          - events:PutTargets\n                          - events:PutRule\n                          - events:ListTargetsByRule\n                          - events:RemoveTargets\n                          - events:DeleteRule\n                          - servicecatalog:ProvisionProduct\n                          - servicecatalog:ListProvisioningArtifacts\n                          - servicecatalog:ListLaunchPaths\n                          - servicecatalog:AssociatePrincipalWithPortfolio\n                          - servicecatalog:ListPortfoliosForProduct\n                          - servicecatalog:SearchProductsAsAdmin\n                          - servicecatalog:TerminateProduct\n                          - servicecatalog:DescribeRecord\n                          - iam:GetRole\n                          - !If [ ControlTowerModeEnabled, \"*\", !Ref 'AWS::NoValue' ] # Not my fault: https://docs.aws.amazon.com/controltower/latest/userguide/setting-up.html#setting-up-iam\n                        Resource:\n                          - '*'\n                      - Effect: Allow\n                        Action:\n                          - secretsmanager:GetSecretValue\n                        Resource:\n                          - !Ref AutomationCredentials\n                      - Effect: Allow\n                        Action:\n                          - ssm:GetParameter\n                          - ssm:PutParameter\n                        Resource:\n                          - !Sub \"arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${ConnectProperties}\"\n                          - !Sub \"arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${SSOProperties}\"\n\n    EmailBucket:\n        DependsOn:\n          - BucketPermission\n        Type: AWS::S3::Bucket\n        Properties:\n            BucketName: !Sub \"accountcontroller-email-processing-${AWS::Region}-${AWS::AccountId}\" # Required, see https://aws.amazon.com/premiumsupport/knowledge-center/unable-validate-destination-s3/\n            LifecycleConfiguration:\n                Rules:\n                  - NoncurrentVersionExpirationInDays: 14\n                    ExpirationInDays: 14\n                    Status: Enabled\n            NotificationConfiguration:\n                LambdaConfigurations:\n                  - Event: \"s3:ObjectCreated:*\"\n                    Function: !GetAtt LambdaFunction.Arn\n            BucketEncryption:\n                ServerSideEncryptionConfiguration:\n                  - ServerSideEncryptionByDefault:\n                        SSEAlgorithm: AES256\n            PublicAccessBlockConfiguration:\n                BlockPublicAcls: true\n                BlockPublicPolicy: true\n                IgnorePublicAcls: true\n                RestrictPublicBuckets: true\n    \n    BucketPermission:\n        Type: AWS::Lambda::Permission\n        Properties:\n            Action: lambda:InvokeFunction\n            FunctionName: !Ref LambdaFunction\n            Principal: s3.amazonaws.com\n            SourceAccount: !Ref AWS::AccountId\n            SourceArn: !Sub arn:aws:s3:::accountcontroller-email-processing-${AWS::Region}-${AWS::AccountId}\n    \n    ReceiptRuleSet:\n        DeletionPolicy: Retain # Custom Resource is responsible for deletion\n        Type: AWS::SES::ReceiptRuleSet\n        Properties:\n            RuleSetName: account-controller\n\n    ReceiptRule:\n        DependsOn:\n          - ReceivedEmailBucketPolicy\n        Type: AWS::SES::ReceiptRule\n        Properties:\n            RuleSetName: !Ref ReceiptRuleSet\n            Rule:\n                Name: default\n                Enabled: true\n                Actions:\n                  - S3Action:\n                        BucketName: !Ref EmailBucket\n    \n    ReceivedEmailBucketPolicy:\n        Type: AWS::S3::BucketPolicy\n        Properties:\n            Bucket: !Ref EmailBucket\n            PolicyDocument:\n                Statement:\n                  - Effect: Allow\n                    Principal:\n                        Service: lambda.amazonaws.com\n                    Action:\n                      - s3:GetObject\n                    Resource:\n                      - !Sub \"${EmailBucket.Arn}/*\"\n                    Condition:\n                        StringEquals:\n                            \"aws:Referer\":\n                              - !Ref \"AWS::AccountId\"\n                  - Effect: Allow\n                    Principal:\n                        Service: ses.amazonaws.com\n                    Action:\n                      - s3:PutObject\n                    Resource:\n                      - !Sub \"${EmailBucket.Arn}/*\"\n                    Condition:\n                        StringEquals:\n                            \"aws:Referer\":\n                              - !Ref \"AWS::AccountId\"\n\n    AutomationUser:\n        DependsOn:\n          - AutomationCredentials\n        Type: AWS::IAM::User\n        Properties:\n            UserName: !Sub \"{{resolve:secretsmanager:${AutomationCredentials}:SecretString:username}}\"\n            LoginProfile:\n                Password: !Sub \"{{resolve:secretsmanager:${AutomationCredentials}:SecretString:password}}\"\n            Policies:\n              - PolicyName: root\n                PolicyDocument:\n                    Version: '2012-10-17'\n                    Statement:\n                      - Effect: Allow\n                        Action:\n                          - '*' # TODO: Fix. I'm sorry, I have no idea what magic the console is doing.\n                        Resource: '*'\n    \n    AutomationCredentials:\n        Type: AWS::SecretsManager::Secret\n        Properties:\n            Name: account-controller-automation-secret\n            Description: Contains secret data for account automation\n            GenerateSecretString:\n                SecretStringTemplate: !Sub |\n                    {\n                        \"username\": \"${AutomationUsername}\",\n                        \"ccmonth\": \"${CCMonth}\",\n                        \"ccyear\": \"${CCYear}\",\n                        \"ccname\": \"${CCName}\",\n                        \"ccnumber\": \"${CCNumber}\"\n                    }\n                GenerateStringKey: \"password\"\n                PasswordLength: 30\n                ExcludeCharacters: '\"@/\\'\n    \n    ConnectSetup:\n        Condition: AccountDeletionEnabled\n        DependsOn:\n          - LambdaLogGroup\n          - ReceiptRuleSet\n          - ConnectProperties\n        Type: Custom::ConnectSetup\n        Properties: \n            ServiceToken: !GetAtt LambdaFunction.Arn\n\n    ConnectProperties:\n        Type: AWS::SSM::Parameter\n        Properties:\n            Type: String\n            Value: '{}'\n            Description: Account Controller properties for Amazon Connect\n    \n    SSOSetup:\n        Condition: AccountCreationEnabled\n        DependsOn:\n          - LambdaLogGroup\n          - SSOProperties\n        Type: Custom::SSOSetup\n        Properties: \n            ServiceToken: !GetAtt LambdaFunction.Arn\n            SSOManagerAppName: !Ref SSOManagerAppName\n            APIGatewayEndpoint: !Sub \"https://${SAMLHttpApi}.execute-api.${AWS::Region}.amazonaws.com\"\n\n    SSOProperties:\n        Type: AWS::SSM::Parameter\n        Properties:\n            Type: String\n            Value: '{}'\n            Description: Account Controller properties for AWS SSO\n\n    SAMLHttpApi:\n        Condition: AccountCreationEnabled\n        Type: AWS::ApiGatewayV2::Api\n        Properties:\n            Name: !Ref AWS::StackName\n            ProtocolType: HTTP\n            RouteSelectionExpression: \"$request.method $request.path\"\n            Version: \"1.0\"\n            CorsConfiguration:\n                AllowMethods:\n                  - GET\n                  - POST\n                  - PUT\n                  - DELETE\n                AllowOrigins:\n                  - \"*\"\n\n    SAMLHttpApiStage:\n        Condition: AccountCreationEnabled\n        Type: AWS::ApiGatewayV2::Stage\n        Properties:\n            ApiId: !Ref SAMLHttpApi\n            AutoDeploy: true\n            StageName: \"$default\"\n\n    SAMLHttpApiGetRoute:\n        Condition: AccountCreationEnabled\n        Type: AWS::ApiGatewayV2::Route\n        Properties:\n            ApiId: !Ref SAMLHttpApi\n            AuthorizationType: NONE\n            RouteKey: \"GET /\"\n            Target: !Sub \"integrations/${SAMLHttpApiIntegration}\"\n\n    SAMLHttpApiPostRoute:\n        Condition: AccountCreationEnabled\n        Type: AWS::ApiGatewayV2::Route\n        Properties:\n            ApiId: !Ref SAMLHttpApi\n            AuthorizationType: NONE\n            RouteKey: \"POST /\"\n            Target: !Sub \"integrations/${SAMLHttpApiIntegration}\"\n\n    SAMLHttpApiPostAccountsRoute:\n        Condition: AccountCreationEnabled\n        Type: AWS::ApiGatewayV2::Route\n        Properties:\n            ApiId: !Ref SAMLHttpApi\n            AuthorizationType: NONE\n            RouteKey: \"POST /accounts\"\n            Target: !Sub \"integrations/${SAMLHttpApiIntegration}\"\n\n    SAMLHttpApiPostDeleteaccountRoute:\n        Condition: AccountCreationEnabled\n        Type: AWS::ApiGatewayV2::Route\n        Properties:\n            ApiId: !Ref SAMLHttpApi\n            AuthorizationType: NONE\n            RouteKey: \"POST /deleteaccount\"\n            Target: !Sub \"integrations/${SAMLHttpApiIntegration}\"\n\n    SAMLHttpApiPostCreateaccountRoute:\n        Condition: AccountCreationEnabled\n        Type: AWS::ApiGatewayV2::Route\n        Properties:\n            ApiId: !Ref SAMLHttpApi\n            AuthorizationType: NONE\n            RouteKey: \"POST /createaccount\"\n            Target: !Sub \"integrations/${SAMLHttpApiIntegration}\"\n\n    SAMLHttpApiIntegration:\n        Condition: AccountCreationEnabled\n        Type: AWS::ApiGatewayV2::Integration\n        Properties:\n            ApiId: !Ref SAMLHttpApi\n            ConnectionType: INTERNET\n            IntegrationMethod: POST\n            IntegrationType: AWS_PROXY\n            IntegrationUri: !GetAtt LambdaFunction.Arn\n            PayloadFormatVersion: \"2.0\"\n\n    AccountDeletionSNSTopic:\n        Condition: AccountDeletionEnabled\n        Type: AWS::SNS::Topic\n\n    AccountDeletionSNSTopicLambdaSubscription:\n        Condition: AccountDeletionEnabled\n        Type: AWS::SNS::Subscription\n        Properties:\n            Protocol: lambda\n            TopicArn: !Ref AccountDeletionSNSTopic\n            Endpoint: !GetAtt LambdaFunction.Arn\n          \n    AccountDeletionSNSTopicPolicy:\n        Condition: AccountDeletionEnabled\n        Type: AWS::SNS::TopicPolicy\n        Properties:\n            PolicyDocument:\n                Version: '2012-10-17'\n                Statement:\n                  - Effect: Allow\n                    Principal:\n                        AWS: \"*\"\n                    Action:\n                      - sns:Publish\n                    Resource: !Ref AccountDeletionSNSTopic\n                    Condition:\n                        ArnLike:\n                            aws:SourceArn: arn:aws:cloudwatch:us-east-1:*:alarm:AccountManagerDeletionBudgetMonitor\n            Topics:\n              - !Ref AccountDeletionSNSTopic\n"
  }
]