Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SAML group for cloudservices_aws_taskcluster_devs #494

Merged
merged 1 commit into from
Jan 20, 2025

Conversation

bheesham
Copy link
Member

@bheesham bheesham commented Jan 20, 2025

Jira: IAM-1545

Link to group in the People Directory: cloudservices_aws_taskcluster_devs


Terraform plan for prod

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # auth0_action.awsSaml will be updated in-place
  ~ resource "auth0_action" "awsSaml" {
      ~ code       = <<-EOT
            const AWS = require("aws-sdk");
            
            exports.onExecutePostLogin = async (event, api) => {
              console.log("Running actions:", "awsSaml");
            
              // Only continue on auth0 prod tenant
              if (event.tenant.id !== "auth") {
                console.log(`Skipping awsSAML; tenant is ${event.tenant.id}`);
                return;
              }
            
              var paramObj = {};
            
              const clientID = event.client.client_id || "";
              switch (clientID) {
                case "JR8HkyiM2i00ma2d1X2xfgdbEHzEYZbS":
                  // IT billing account params
                  paramObj.region = "us-west-2";
                  paramObj.IdentityStoreId = event.secrets.AWS_IDENTITYSTORE_ID_IT;
                  paramObj.accessKeyId = event.secrets.AWS_IDENTITYSTORE_ACCESS_ID_IT;
                  paramObj.secretAccessKey = event.secrets.AWS_IDENTITYSTORE_ACCESS_KEY_IT;
                  paramObj.awsGroups = [
                    "fuzzing_team",
                    "mozilliansorg_aws_billing_access",
                    "mozilliansorg_cia-aws",
                    "mozilliansorg_consolidated-billing-aws",
                    "mozilliansorg_consolidated-billing-aws-readonly",
                    "mozilliansorg_discourse-devs",
                    "mozilliansorg_http-observatory-rds",
                    "mozilliansorg_iam-admins",
                    "mozilliansorg_iam-in-transition",
                    "mozilliansorg_iam-in-transition-admin",
                    "mozilliansorg_iam-readonly",
                    "mozilliansorg_meao-admins",
                    "mozilliansorg_mozilla-moderator-devs",
                    "mozilliansorg_partinfra-aws",
                    "mozilliansorg_pdfjs-testers",
                    "mozilliansorg_pocket_cloudtrail_readers",
                    "mozilliansorg_project-guardian-admins",
                    "mozilliansorg_relay_developer",
                    "mozilliansorg_searchfox-aws",
                    "mozilliansorg_secops-aws-admins",
                    "mozilliansorg_sre",
                    "mozilliansorg_sumo-admins",
                    "mozilliansorg_sumo-devs",
                    "mozilliansorg_voice_aws_admin_access",
                    "mozilliansorg_web-sre-aws-access",
                    "mozilliansorg_webcompat-alexa-admins",
                    "team_mdn",
                    "team_netops",
                    "team_opsec",
                    "team_se",
                    "team_secops",
                    "voice-dev",
                    "vpn_sumo_aws_devs"
                  ];
                  break;
                case "pQ0eb5tzwfYHnAtzGuk88pzxZ68szQtk":
                  // Pocket Billing Account
                  paramObj.region = "us-east-1";
                  paramObj.IdentityStoreId = event.secrets.AWS_IDENTITYSTORE_ID_POCKET;
                  paramObj.accessKeyId = event.secrets.AWS_IDENTITYSTORE_ACCESS_ID_POCKET;
                  paramObj.secretAccessKey =
                    event.secrets.AWS_IDENTITYSTORE_ACCESS_KEY_POCKET;
                  paramObj.awsGroups = [
                    "mozilliansorg_pocket_admin",
                    "mozilliansorg_pocket_backend",
                    "mozilliansorg_pocket_backup_admin",
                    "mozilliansorg_pocket_backup_readonly",
                    "mozilliansorg_pocket_cloudtrail_readers",
                    "mozilliansorg_pocket_dataanalytics",
                    "mozilliansorg_pocket_datalearning",
                    "mozilliansorg_pocket_developer",
                    "mozilliansorg_pocket_fin_ops",
                    "mozilliansorg_pocket_frontend",
                    "mozilliansorg_pocket_marketing",
                    "mozilliansorg_pocket_mozilla_sre",
                    "mozilliansorg_pocket_qa",
                    "mozilliansorg_pocket_readonly",
                    "mozilliansorg_pocket_sales",
                    "mozilliansorg_pocket_ads",
                    "mozilliansorg_pocket_aws_billing",
                    "mozilliansorg_infrasec"
                  ];
                  break;
                case "jU8r4uSEF3fUCjuJ63s46dBnHAfYMYfj":
                  // MoFo Billing Account
                  paramObj.region = "us-east-2";
                  paramObj.IdentityStoreId = event.secrets.AWS_IDENTITYSTORE_ID_MOFO;
                  paramObj.accessKeyId = event.secrets.AWS_IDENTITYSTORE_ACCESS_ID_MOFO;
                  paramObj.secretAccessKey =
                    event.secrets.AWS_IDENTITYSTORE_ACCESS_KEY_MOFO;
                  paramObj.awsGroups = [
                    "mozilliansorg_mofo_aws_admins",
                    "mozilliansorg_mofo_aws_community",
                    "mozilliansorg_mofo_aws_everything",
                    "mozilliansorg_mofo_aws_labs",
                    "mozilliansorg_mofo_aws_projects",
                    "mozilliansorg_mofo_aws_sandbox",
                    "mozilliansorg_mofo_aws_secure",
                    "mozilliansorg_infrasec"
                  ];
                  break;
                case "c0x6EoLdp55H2g2OXZTIUuaQ4v8U4xf9":
                  // CloudServices billing account params
                  paramObj.region = "us-west-2";
                  paramObj.IdentityStoreId = event.secrets.AWS_IDENTITYSTORE_ID_CLOUDSERVICES;
                  paramObj.accessKeyId = event.secrets.AWS_IDENTITYSTORE_ACCESS_ID_CLOUDSERVICES;
                  paramObj.secretAccessKey = event.secrets.AWS_IDENTITYSTORE_ACCESS_KEY_CLOUDSERVICES;
                  paramObj.awsGroups = [
                    "mozilliansorg_aws_billing_access",
                    "mozilliansorg_cloudservices_aws_admin",
                    "mozilliansorg_cloudservices_aws_autograph_admin",
                    "mozilliansorg_cloudservices_aws_autograph_dev",
                    "mozilliansorg_cloudservices_aws_dev_developers",
                    "mozilliansorg_cloudservices_aws_developer_services_dev",
                    "mozilliansorg_cloudservices_aws_fxa_developers",
          +         "mozilliansorg_cloudservices_aws_taskcluster_devs",
                    "mozilliansorg_infrasec"
                  ];
                  break;
                default:
                  return; // Not an AWS login, continue auth pipeline
              }
            
              // Instantate and set Region
              var i = new AWS.IdentityStore({
                region: paramObj.region,
                apiVersion: "2020-06-15",
                accessKeyId: paramObj.accessKeyId,
                secretAccessKey: paramObj.secretAccessKey,
              });
            
              const IdentityStoreId = paramObj.IdentityStoreId;
              const userName = event.user.email;
              var AWSUserId = "";
            
              // This is a list of groups that are mapped to AWS groups
              const AWS_GROUPS = paramObj.awsGroups;
            
              // Filter the users Auth0 groups down to only those mapped to AWS groups
              function filterAWSGroups(groups) {
                var filteredGroups = groups.filter((x) => AWS_GROUPS.includes(x));
                return filteredGroups;
              }
            
              function userAuth0Groups(proposedGroups, existingGroups) {
                var addToGroup = proposedGroups.filter((x) => !existingGroups.includes(x));
                var removeFromGroup = existingGroups.filter(
                  (x) => !proposedGroups.includes(x)
                );
                return { addToGroup: addToGroup, removeFromGroup: removeFromGroup };
              }
            
              function createGroupMemberships(addToGroup) {
                var creationPromises = [];
                for (var groupId of addToGroup) {
                  var params = {
                    IdentityStoreId: IdentityStoreId,
                    GroupId: groupId,
                    MemberId: {
                      UserId: AWSUserId,
                    },
                  };
                  creationPromises.push(i.createGroupMembership(params).promise());
                }
                return Promise.all(creationPromises);
              }
            
              function removeGroupMemberships(removeMembershipId) {
                var removalPromises = [];
                for (var membershipId of removeMembershipId) {
                  var params = {
                    IdentityStoreId: IdentityStoreId,
                    MembershipId: membershipId,
                  };
                  removalPromises.push(i.deleteGroupMembership(params).promise());
                }
                return Promise.all(removalPromises);
              }
            
              function fetchAWSUUID() {
                var params = {
                  Filters: [
                    {
                      AttributePath: "UserName",
                      AttributeValue: userName,
                    },
                  ],
                  IdentityStoreId: IdentityStoreId,
                };
                var userId = i.listUsers(params).promise();
                return userId; // returns promise
              }
            
              function fetchUsersAWSGroups(userUUID) {
                var params = {
                  IdentityStoreId: IdentityStoreId,
                  MemberId: {
                    UserId: userUUID,
                  },
                  MaxResults: 50,
                };
                // TODO: handle pagenation!!!
                var userMembership = i.listGroupMembershipsForMember(params).promise();
                return userMembership;
              }
            
              function fetchGroupNameMap(groupList) {
                var groupPromises = [];
                for (var group of groupList) {
                  var params = {
                    GroupId: group.GroupId,
                    IdentityStoreId: IdentityStoreId,
                  };
                  groupPromises.push(i.describeGroup(params).promise());
                }
                return Promise.all(groupPromises);
              }
            
              function getGroupIds(groupList) {
                var promisedGroupIds = [];
                for (var groupName of groupList) {
                  var params = {
                    IdentityStoreId: IdentityStoreId,
                    AlternateIdentifier: {
                      UniqueAttribute: {
                        AttributePath: "DisplayName",
                        AttributeValue: groupName,
                      },
                    },
                  };
                  promisedGroupIds.push(i.getGroupId(params).promise());
                }
                return Promise.all(promisedGroupIds);
              }
            
              function createUser() {
                var params = {
                  IdentityStoreId: IdentityStoreId,
                  DisplayName: event.user.name,
                  UserName: event.user.email,
                  Name: {
                    FamilyName: event.user.family_name,
                    GivenName: event.user.given_name,
                  },
                  Emails: [
                    {
                      Primary: true,
                      Value: event.user.email,
                    },
                  ],
                };
                return i.createUser(params).promise();
              }
            
              // Main
              try {
                // Get the users group list filtered down to only AWS related groups
                const proposedGroups = filterAWSGroups(event.user.groups);
            
                // Fetch users AWS UUID
                const userObjList = await fetchAWSUUID();
                if (userObjList.Users.length === 0) {
                  console.log(
                    `[${IdentityStoreId}] Creating User (${userName}) in AWS IdentityStore`
                  );
                  AWSUserId = (await createUser()).UserId;
                } else {
                  AWSUserId = userObjList.Users[0].UserId;
                }
            
                // Get users existing AWS group membership
                const usersAWSGroups = await fetchUsersAWSGroups(AWSUserId);
            
                const usersAWSGroupNames = await fetchGroupNameMap(
                  usersAWSGroups.GroupMemberships
                );
                const existingGroups = usersAWSGroupNames.map((item) => item.DisplayName);
            
                // Diff the proposed groups and the existing groups
                const groupActionList = userAuth0Groups(proposedGroups, existingGroups);
                const addToGroup = groupActionList.addToGroup; // DisplayName list
                const removeFromGroup = groupActionList.removeFromGroup; // DisplayName list
            
                if (addToGroup.length > 0 || removeFromGroup.length > 0) {
                  console.log(
                    `[${IdentityStoreId}] Add user (${userName}) to: `,
                    addToGroup
                  );
                  console.log(
                    `[${IdentityStoreId}] Remove user (${userName}) from: `,
                    removeFromGroup
                  );
            
                  const addToGroupIds = (await getGroupIds(addToGroup)).map(
                    (item) => item.GroupId
                  );
            
                  // From the groupsmembership object, filter and map group ids to be removed from
                  const removeGroupIds = usersAWSGroupNames
                    .filter((item) => removeFromGroup.includes(item.DisplayName))
                    .map((item) => item.GroupId);
                  const removeMembershipId = usersAWSGroups.GroupMemberships.filter(
                    (item) => removeGroupIds.includes(item.GroupId)
                  ).map((item) => item.MembershipId);
            
                  // Create group memberships
                  const addPromise = createGroupMemberships(addToGroupIds);
            
                  // Delete group memberships
                  const removePromise = removeGroupMemberships(removeMembershipId);
                  return Promise.all([addPromise, removePromise]);
                }
            
                return;
              } catch (err) {
                console.error(err);
                return api.access.deny(err);
              }
            }
        EOT
        id         = "feba3651-676a-43e2-8c2e-a1e877100513"
        name       = "awsSaml"
        # (3 unchanged attributes hidden)

        # (14 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

@bheesham bheesham requested review from dividehex and gcoxmoz January 20, 2025 16:46
@bheesham
Copy link
Member Author

bheesham commented Jan 20, 2025

There's a lot of drift for the dev account I'm not going to apply this there, since I don't want to destroy @dividehex 's work.

Though, I'll include the plan here for completion.

Plan for dev

Terraform will perform the following actions:

  # auth0_action.OIDCConformanceWorkaround will be updated in-place
  ~ resource "auth0_action" "OIDCConformanceWorkaround" {
      ~ code       = <<-EOT
          - const { LoggingWinston } = require('@google-cloud/logging-winston');
          - const winston = require('winston');
          - 
            exports.onExecutePostLogin = async (event, api) => {
          -   const serviceAccountCreds = JSON.parse(event.secrets.google_service_account_creds);
          +   console.log("Running action:", "OIDCConformanceWorkaround");
            
          -   // Create a GCP LoggingWinston transport with credentials
          -   const loggingWinston = new LoggingWinston({
          -     projectId: 'iam-auth0',
          -     logName: `auth0-action-${event.tenant.id}`,
          -     credentials: serviceAccountCreds,
          -     labels: {
          -       tenant: event.tenant.id || null,
          -       client_id: event.client.client_id || null,
          -       client_name: event.client.name || null,
          -       connection_id: event.connection.id || null,
          -       connection_name: event.connection.name || null,
          -       connection_strategy: event.connection.strategy || null,
          -       session_id: event.session.id || null,
          -       user_id: event.user.user_id || null,
          -       user_email: event.user.email || null,
          -       service: 'auth0-actions',
          -     },
          -   });
          - 
          -   // Create the Winston logger
          -   const logger = winston.createLogger({
          -     level: 'info', // Set the minimum log level
          -     transports: [
          -       // Add Google Cloud Logging transport
          -       loggingWinston,
          - 
          -       // Also log to the console
          -       new winston.transports.Console({
          -         format: winston.format.combine(
          -           winston.format.colorize(),
          -           winston.format.simple()
          -         ),
          -       }),
          -     ],
          -   });
          - 
          -   logger.info("Running action: OIDCConformanceWorkaround");
          - 
              // This issue only affects certain application
              var apps = [
                'UCOY390lYDxgj5rU8EeXRtN6EP005k7V', // sso dashboard prod
                '2KNOUCxN8AFnGGjDCGtqiDIzq8MKXi2h', // sso dashboard allizom
              ];
            
              if (apps.indexOf(event.client.client_id) >= 0) {
                // Fix http://openid.net/specs/openid-connect-implicit-1_0.html#StandardClaims
                // This ensures updated_at is an INTEGER (timestamp since the epoch) and not a string
                // So that libraries that follow the OpenID Connect spec function as intended.
                api.idToken.setCustomClaim(`updated_at`, Math.floor(Number(new Date(event.user.updated_at))/1000));
                return;
              }
              return;
            }
        EOT
        id         = "53db6101-6d95-4057-b3b5-b322b8ceb46b"
        name       = "OIDCConformanceWorkaround"
        # (3 unchanged attributes hidden)

      - dependencies {
          - name    = "@google-cloud/logging-winston" -> null
          - version = "6.0.0" -> null
        }
      - dependencies {
          - name    = "winston" -> null
          - version = "3.17.0" -> null
        }

      - secrets {
          - name  = "google_service_account_creds" -> null
          - value = (sensitive value) -> null
        }

        # (1 unchanged block hidden)
    }

  # auth0_action.accessRules will be updated in-place
  ~ resource "auth0_action" "accessRules" {
      ~ code       = <<-EOT
            // Required Libraries
          - const { LoggingWinston } = require('@google-cloud/logging-winston');
          - const winston = require('winston');
          + const fetch = require('node-fetch');
            const YAML = require('js-yaml');
            const jwt = require('jsonwebtoken');
            const AWS = require('aws-sdk');
            
            
            exports.onExecutePostLogin = async (event, api) => {
          +   console.log("Running actions:", "accessRules");
            
          -   const serviceAccountCreds = JSON.parse(event.secrets.google_service_account_creds);
          - 
          -   // Create a GCP LoggingWinston transport with credentials
          -   const loggingWinston = new LoggingWinston({
          -     projectId: 'iam-auth0',
          -     logName: `auth0-action-${event.tenant.id}`,
          -     credentials: serviceAccountCreds,
          -     labels: {
          -       tenant: event.tenant.id || null,
          -       client_id: event.client.client_id || null,
          -       client_name: event.client.name || null,
          -       connection_id: event.connection.id || null,
          -       connection_name: event.connection.name || null,
          -       connection_strategy: event.connection.strategy || null,
          -       session_id: event.session.id || null,
          -       user_id: event.user.user_id || null,
          -       user_email: event.user.email || null,
          -       service: 'auth0-actions',
          -     },
          -   });
          - 
          -   // Create the Winston logger
          -   const logger = winston.createLogger({
          -     level: 'info', // Set the minimum log level
          -     transports: [
          -       // Add Google Cloud Logging transport
          -       loggingWinston,
          - 
          -       // Also log to the console
          -       new winston.transports.Console({
          -         format: winston.format.combine(
          -           winston.format.colorize(),
          -           winston.format.simple()
          -         ),
          -       }),
          -     ],
          -   });
          - 
          -   logger.info("Running actions: accessRules");
          - 
              // Retrieve and return a secret from AWS Secrets Manager
              const getSecrets = async () => {
                try {
                  if (!event.secrets.accessKeyId || !event.secrets.secretAccessKey) {
                    throw new Error('AWS access keys are not defined.');
                  }
            
                  // Set up AWS client
                  AWS.config.update({
                    region: 'us-west-2',
                    accessKeyId: event.secrets.accessKeyId,
                    secretAccessKey: event.secrets.secretAccessKey
                  });
            
                  const secretsManager = new AWS.SecretsManager();
                  const secretPath = event.tenant.id === "dev" ? "/iam/auth0/dev/actions" : "/iam/auth0/prod/actions";
                  const data = await secretsManager.getSecretValue({ SecretId: secretPath }).promise();
                  // handle string or binary
                  if ('SecretString' in data) {
                    return JSON.parse(data.SecretString);
                  } else {
                    let buff = Buffer.from(data.SecretBinary, 'base64');
                    return buff.toString('ascii');
                  }
                } catch (err) {
          -       logger.info("getSecrets:" + err);
          +       console.log("getSecrets:", err);
                  throw err;
                };
              }
            
              // Load secrets
              const secrets = await getSecrets();
            	const jwtMsgsRsaSkey = secrets.jwtMsgsRsaSkey;
            
              // postError(code)
              // @code string with an error code for the SSO Dashboard to display
              // @rcontext the current Auth0 rule context (passed from the rule)
              // Returns rcontext with redirect set to the error
            
              const postError = (code, prefered_connection_arg) => {
                try {
                  const prefered_connection = prefered_connection_arg || ""; // Optional arg
                  if (!jwtMsgsRsaSkey) {
                    throw new Error('jwtMsgsRsaSkey is not defined.');
                  }
                  // Token is valid from 30s ago, to 1h from now
                  const skey = Buffer.from(jwtMsgsRsaSkey, 'base64').toString('ascii');
                  const token = jwt.sign(
                    {
                      client: event.client.name,
                      code: code,
                      connection: event.connection.name,
                      exp: Math.floor(Date.now() / 1000) + 3600,
                      iat: Math.floor(Date.now() / 1000) - 30,
                      preferred_connection_name: prefered_connection,
                      redirect_uri: event.transaction.redirect_uri,
                    },
                    skey,
                    { algorithm: 'RS256' }
                  );
            
                  const domain = event.tenant.id === "dev" ? "sso.allizom.org" : "sso.mozilla.com";
                  const forbiddenUrl = new URL(`https://${domain}/forbidden`);
                  forbiddenUrl.searchParams.set("error", token);
                  api.redirect.sendUserTo(forbiddenUrl.href);
            
                  return;
                } catch (err) {
          -       logger.error("postError:" + err);
          +       console.log("postError:", err);
                  throw err;
                }
              }
            
              if (!event.user.email_verified) {
          -     logger.error(`User primary email NOT verified, refusing login for ${event.user.email}`);
          +     console.log(`User primary email NOT verified, refusing login for ${event.user.email}`);
                // This post error is broken in sso dashboard
                postError("primarynotverified", event, api, jwt, jwtMsgsRsaSkey);
                return;
              }
            
              const namespace = 'https://sso.mozilla.com/claim';
            
              // MFA bypass for special service accounts
              const mfaBypassAccounts = [
                '[email protected]',      // MOC see: https://bugzilla.mozilla.org/show_bug.cgi?id=1423903
                '[email protected]',  // MOC see: https://bugzilla.mozilla.org/show_bug.cgi?id=1423903
              ];
            
              const duoConfig = {
                "host": event.secrets.duo_apihost_mozilla,
                "ikey": event.secrets.duo_ikey_mozilla,
                "skey": event.secrets.duo_skey_mozilla,
                "username": event.user.email,
              };
            
              // Check if array A has any occurrence from array B
              const hasCommonElements = (A, B) => {
                  return A.some(element => B.includes(element));
              }
            
              // Process the access cache decision
              const access_decision = (access_rules, access_file_conf) => {
                // Ensure we have the correct group data
                const app_metadata_groups = event.user.app_metadata.groups || [];
                const ldap_groups = event.user.ldap_groups || [];
                const user_groups = event.user.groups || [];
            
                // With account linking its possible that LDAP is not the main account on contributor LDAP accounts
                // Here we iterate over all possible user identities and build an array of all groups from them
                let _identity;
                let identityGroups = [];
                // Iterate over each identity
                for (let x = 0, len = event.user.identities.length; x<len; x++) {
                  // Get profile for the given identity
                  _identity = event.user.identities[x];
                  // If the identity contains profileData
                  if ("profileData" in _identity) {
                    // If profileData contains a groups array
                    if ("groups" in _identity.profileData) {
                      // Merge the group arry into identityGroups
                      identityGroups.push(..._identity.profileData.groups);
                    }
                  }
                }
            
                // Collect all variations of groups and merge them together for access evaluation
                let groups = [...user_groups, ...app_metadata_groups, ...ldap_groups, ...identityGroups];
            
                // Inject the everyone group and filter for duplicates
                groups.push("everyone");
                groups = groups.filter((value, index, array) => array.indexOf(value) === index);
            
            
                // If the only scopes requested are neither profile nor any scope beginning with
                // https:// then do not overload with custom claims
                const scopes_requested = event.transaction.requested_scopes || [];
            
                let fixup_needed = (scope) => {
                  return scope === 'profile' || scope.startsWith('https://');
                };
            
                if (scopes_requested.some(fixup_needed)) {
          -       logger.info(`Client ${event.client.client_id} requested ${scopes_requested}, therefore adding custom claims`);
          +       console.log(`Client ${event.client.client_id} requested ${scopes_requested}, therefore adding custom claims`);
                  api.idToken.setCustomClaim("email_aliases", undefined);
                  api.idToken.setCustomClaim("dn", undefined);
                  api.idToken.setCustomClaim("organizationUnits", undefined);
                  api.idToken.setCustomClaim(`${namespace}/groups`, groups);
            
                  const claimMsg = 'Please refer to https://github.com/mozilla-iam/person-api in order to query Mozilla IAM CIS user profile data';
                  api.idToken.setCustomClaim(`${namespace}/README_FIRST`, claimMsg);
                }
            
                //// === Actions don't allow modifying the event.user
                //// Update user.groups with new merged values
                //user.groups = groups;
            
                // This is used for authorized user/groups
                let authorized = false;
                // Defaut app requested aal to MEDIUM for all apps which do not have this set in access file
                let required_aal = "MEDIUM";
            
                for (let i=0; i<access_rules.length; i++) {
                  let app = access_rules[i].application;
            
                  //Handy for quick testing in dev (overrides access rules)
                  //var app = {'client_id': 'pCGEHXW0VQNrQKURDcGi0tghh7NwWGhW', // This is testrp social-ldap-pwless
                  //           'authorized_users': ['[email protected]'],
                  //           'authorized_groups': ['okta_mfa'],
                  //           'aal': 'LOW'
                  //          };
            
            
                  if (app.client_id && (app.client_id.indexOf(event.client.client_id) >= 0)) {
                    // If there are multiple applications in apps.yml with the same client_id
                    // then this expiration of access check will only run against the first
                    // one encountered. This matters if there are multiple applications, using
                    // the same client_id, and asserting different expire_access_when_unused_after
                    // values.
            
                    // Set app AAL (AA level) if present
                    required_aal = app.AAL || required_aal;
            
                    // AUTHORIZED_{GROUPS,USERS}
                    // XXX this authorized_users SHOULD BE REMOVED as it's unsafe (too easy to make mistakes). USE GROUPS.
                    // XXX This needs to be fixed in the dashboard first
                    // Empty users or groups (length == 0) means no access in the dashboard apps.yml world
                    if (app.authorized_users.length === 0 && app.authorized_groups.length === 0) {
                      const msg = `Access denied to ${event.client.client_id} for user ${event.user.email} (${event.user.user_id})`
                        + ` - this app denies ALL users and ALL groups")`;
          - 					logger.info(msg);
          + 					console.log(msg);
                      return 'notingroup';
                    }
            
                    // Check if the user is authorized to access
                    // A user is authorized if they are a member of any authorized_groups or if they are one of the authorized_users
                    if ((app.authorized_users.length > 0 ) && (app.authorized_users.indexOf(event.user.email) >= 0)) {
                      authorized = true;
                    // Same dance as above, but for groups
                    } else if ((app.authorized_groups.length > 0) && hasCommonElements(app.authorized_groups, groups)) {
                      authorized = true;
                    } else {
                      authorized = false;
                    }
            
                    if (!authorized) {
                      const msg = `Access denied to ${event.client.client_id} for user ${event.user.email} (${event.user.user_id})`
                      + ` - not in authorized group or not an authorized user`;
          -           logger.info(msg);
          +           console.log(msg);
                      return 'notingroup';
                    }
                  } // correct client id / we matched the current RP
                } // for loop / next rule in apps.yml
            
            
                // AAI (AUTHENTICATOR ASSURANCE INDICATOR)
                // Sets the AAI for the user. This is later used by the AccessRules.js rule which also sets the AAL.
            
                // We go through each possible attribute as auth0 will translate these differently in the main profile
                // depending on the connection type
            
                const getProfileData = (connection) => {
                  // Return a single identity by connection name, from the user structure
                  var i = 0;
                  for (i=0; i < event.user.identities.length; i++) {
                    var cid = event.user.identities[i];
                    if (cid.connection === connection) {
                      return cid.profileData;
                    }
                  }
            
                  return undefined;
                } // getProfileData func
            
                // Ensure all users have some AAI and AAL attributes, even if its empty
                let aai = [];
                let aal = "UNKNOWN";
            
                // Allow certain LDAP service accounts to fake their MFA. For all other LDAPi accounts, enforce MFA
                if (event.connection.strategy === "ad") {
                  if (mfaBypassAccounts.includes(event.user.email)) {
          -         logger.info(`LDAP service account (${event.user.email}) is allowed to bypass MFA`);
          +         console.log(`LDAP service account (${event.user.email}) is allowed to bypass MFA`);
                    aai.push("2FA");
                  } else {
                    api.multifactor.enable("duo", { "providerOptions": duoConfig, "allowRememberBrowser": true });
          -         logger.info(`duosecurity: ${event.user.email} is in LDAP and requires 2FA check`);
          +         console.log(`duosecurity: ${event.user.email} is in LDAP and requires 2FA check`);
                  }
                }
            
                const profileData = getProfileData(event.connection.name);
            
                //GitHub attribute
                if (event.connection.name === "github") {
                  if ((event.user.two_factor_authentication !== undefined) && (event.user.two_factor_authentication === true)) {
                    aai.push("2FA");
                  } else if ((profileData !== undefined) && (profileData.two_factor_authentication === true)) {
                    aai.push("2FA");
                  }
                // Firefox Accounts
                } else if (event.connection.name === "firefoxaccounts") {
                  if ((event.user.fxa_twoFactorAuthentication !== undefined) && (event.user.fxa_twoFactorAuthentication === true)) {
                    aai.push("2FA");
                  } else if ((profileData !== undefined) && (profileData.fxa_twoFactorAuthentication === true)) {
                    aai.push("2FA");
                  }
                // LDAP/DuoSecurity
                } else if ((event.user.multifactor !== undefined) && (event.user.multifactor[0] === "duo")) {
                  aai.push("2FA");
                } else if (event.connection.name === 'google-oauth2') {
                  // We set Google to HIGH_ASSURANCE_IDP which is a special indicator, this is what it represents:
                  // - has fraud detection
                  // - will inform users when their account is used or logged through push notifications on their devices
                  // - will actively block detected fraudulent logins even with correct credentials
                  // - will fallback to phone 2FA in most cases (old accounts may still bypass that in some cases)
                  // - will fallback to phone 2FA on all recent accounts
                  // Note that this is not the same as "2FA" and other indicators, as we simply do not have a technically accurate
                  // indicator of what the authenticator supports at this time for Google accounts
                  aai.push("HIGH_ASSURANCE_IDP");
                }
            
                // AAI (AUTHENTICATOR ASSURANCE INDICATOR) REQUIREMENTS
                //
                // Note that user.aai is set in another rule (rules/aai.js)
                // This file sets the user.aal (authenticator assurance level) which is the result of a map lookup against user.aai
                //
                // Mapping logic and verification
                // Ex: our mapping says 2FA for MEDIUM AAL and app AAL is MEDIUM as well, and the user has 2FA AAI, looks like:
                // access_file_conf.aai_mapping['MEDIUM'] = ['2FA'];
                // app.AAL = 'MEDIUM;
                // user.aai = ['2FA'];
                // Thus user should be allowed for this app (it requires MEDIUM, and MEDIUM requires 2FA, and user has 2FA
                // indeed)
                //
                let aai_pass = false;
                if (access_file_conf.aai_mapping !== undefined) {
                  // 1 Set user.aal
                  // maps = [ "LOW", "MEDIUM", ...
                  // aal_nr = position in the maps (aai_mapping[maps[aal_nr=0]] is "LOW" for.ex)
                  // aai_nr = position in the array of AAIs (aai_mapping[maps[aal_nr=0]] returns ["2FA", .., aai_nr=0 would be the
                  // position for "2FA")
                  // Note that the list is ordered so that the highest AAL always wins
                  const maps = Object.keys(access_file_conf.aai_mapping);
                  for (let aal_nr = 0; aal_nr < maps.length; aal_nr++) {
                    for (let aai_nr = 0; aai_nr < access_file_conf.aai_mapping[maps[aal_nr]].length; aai_nr++) {
                      let cur_aai = access_file_conf.aai_mapping[maps[aal_nr]][aai_nr];
                      if (aai.indexOf(cur_aai) >= 0) {
                        aal = maps[aal_nr];
          -             logger.info(`User AAL set to ${aal} because AAI contains ${aai}`);
          +             console.log(`User AAL set to ${aal} because AAI contains ${aai}`);
                        break;
                      }
                    }
                  }
                  // 2 Check if user.aal is allowed for this RP
                  if (access_file_conf.aai_mapping[required_aal].length === 0) {
          -         logger.info("No required indicator in aai_mapping for this RP (mapping empty for this AAL), access will be granted");
          +         console.log("No required indicator in aai_mapping for this RP (mapping empty for this AAL), access will be granted");
                    aai_pass = true;
                  } else {
                    for (let y = 0; y < aai.length; y++) {
                      let this_aai = aai[y];
                      if (access_file_conf.aai_mapping[required_aal].indexOf(this_aai) >= 0) {
          -             logger.info("User AAL is included in this RP's AAL requirements, access will be granted");
          +             console.log("User AAL is included in this RP's AAL requirements, access will be granted");
                        aai_pass = true;
                        break;
                      }
                    }
                  }
                }
            
                // Set AAI & AAL claims in idToken
                api.idToken.setCustomClaim(`${namespace}/AAI`, aai);
                api.idToken.setCustomClaim(`${namespace}/AAL`, aal);
            
                if (!aai_pass) {
            			const msg = `Access denied to ${event.client.client_id} for user ${event.user.email} (${event.user.user_id}) - due to`
                    + ` Identity Assurance Level being too low for this RP. Required AAL: ${required_aal} (${aai_pass})`
          - 			logger.info(msg);
          + 			console.log(msg);
                  return "aai_failed";
                }
            
                // We matched no rule, access is granted
                return true;
              }
            
            
              const access_file_conf = { aai_mapping: {
                "LOW": [],
                "MEDIUM": ["2FA", "HIGH_ASSURANCE_IDP"],
                "HIGH": ["HIGH_NOT_IMPLEMENTED"],
                "MAXIMUM": ["MAXIMUM_NOT_IMPLEMENTED"]
              }};
            
              // This function pulls the apps.yml and returns a promise to yield the application list
              async function getAppsYaml(url) {
                try {
                    const response = await fetch(url);
                    const data = await response.text();
                    const yamlContent = YAML.load(data);
                    return yamlContent.apps;
                } catch (error) {
          -         logger.error('Error fetching apps.yml:' + error);
          +         console.error('Error fetching apps.yml:', error);
                    throw error;
                }
              }
            
              // Main try
              try {
                const cdnUrl = 'https://cdn.sso.mozilla.com/apps.yml';
                const appsYaml = await getAppsYaml(cdnUrl);
            		const decision = access_decision(appsYaml, access_file_conf);
            
                if (decision === true) {
                  return; // Allow login to continue
                } else {
            			// Go back to the shadow.  You shall not pass!
            			postError(decision);
                  return;
                }
              } catch (err) {
                // All error should be caught here and we return the callback handler with the error
          -     logger.error("AccessRules:" + err);
          +     console.log("AccessRules:", err);
                return api.access.deny(err);
              }
            }
        EOT
        id         = "b2226199-3b5f-4f47-befe-946cc3fc9d4c"
        name       = "accessRules"
        # (3 unchanged attributes hidden)

      - dependencies {
          - name    = "@google-cloud/logging-winston" -> null
          - version = "6.0.0" -> null
        }
      - dependencies {
          - name    = "winston" -> null
          - version = "3.17.0" -> null
        }

      - secrets {
          - name  = "google_service_account_creds" -> null
          - value = (sensitive value) -> null
        }

        # (10 unchanged blocks hidden)
    }

  # auth0_action.activateNewUsersInCIS will be updated in-place
  ~ resource "auth0_action" "activateNewUsersInCIS" {
      ~ code       = <<-EOT
          - const { LoggingWinston } = require('@google-cloud/logging-winston');
          - const winston = require('winston');
          + const fetch = require("node-fetch");
            const jwt = require("jsonwebtoken");
            const AWS = require('aws-sdk');
            
            exports.onExecutePostLogin = async (event, api) => {
          +   console.log("Running action:", "activateNewUsersInCIS");
            
          -   const serviceAccountCreds = JSON.parse(event.secrets.google_service_account_creds);
            
          -   // Create a GCP LoggingWinston transport with credentials
          -   const loggingWinston = new LoggingWinston({
          -     projectId: 'iam-auth0',
          -     logName: `auth0-action-${event.tenant.id}`,
          -     credentials: serviceAccountCreds,
          -     labels: {
          -       tenant: event.tenant.id || null,
          -       client_id: event.client.client_id || null,
          -       client_name: event.client.name || null,
          -       connection_id: event.connection.id || null,
          -       connection_name: event.connection.name || null,
          -       connection_strategy: event.connection.strategy || null,
          -       session_id: event.session.id || null,
          -       user_id: event.user.user_id || null,
          -       user_email: event.user.email || null,
          -       service: 'auth0-actions',
          -     },
          -   });
          - 
          -   // Create the Winston logger
          -   const logger = winston.createLogger({
          -     level: 'info', // Set the minimum log level
          -     transports: [
          -       // Add Google Cloud Logging transport
          -       loggingWinston,
          - 
          -       // Also log to the console
          -       new winston.transports.Console({
          -         format: winston.format.combine(
          -           winston.format.colorize(),
          -           winston.format.simple()
          -         ),
          -       }),
          -     ],
          -   });
          - 
          -   logger.info("Running action: activateNewUsersInCIS");
          - 
          - 
              const WHITELISTED_CONNECTIONS = ['email', 'firefoxaccounts', 'github', 'google-oauth2', 'Mozilla-LDAP', 'Mozilla-LDAP-Dev'];
            
              // We can only provision users that have certain connection strategies
              if (!WHITELISTED_CONNECTIONS.includes(event.connection.name)) {
          -     logger.info(`${event.connection.name} is not whitelisted. Skip activateNewUsersInCIS`);
          +     console.log(`${event.connection.name} is not whitelisted. Skip activateNewUsersInCIS`);
                return;
              }
            
              // If you're explicitly flagged as existing in CIS, then we don't need to continue onward
              if (event.user.app_metadata?.existsInCIS) {
          -     logger.info(`${event.user.user_id} existsInCIS is True.  Skip activateNewUsersInCIS`);
          +     console.log(`${event.user.user_id} existsInCIS is True.  Skip activateNewUsersInCIS`);
                return;
              }
            
              // Consts
              const AUTH0_TIMEOUT = 5000;  // milliseconds
              const CHANGEAPI_TIMEOUT = 14000;  // milliseconds
              const PERSONAPI_BEARER_TOKEN_REFRESH_AGE = 64800;  // 18 hours
              const PERSONAPI_TIMEOUT = 5000;  // milliseconds
              const PUBLISHER_NAME = 'access_provider';
              const USER_ID = event.user.user_id;
            
              // If we don't have the secret variables we need, bail
              // note that this requires the "PersonAPI - Auth0" application configured with the following scopes:
              // classification: public, display: none, display: public, write
              if (!event.secrets.accessKeyId ||
                  !event.secrets.secretAccessKey ||
                  !event.secrets.changeapi_url ||
                  !event.secrets.personapi_oauth_url ||
                  !event.secrets.personapi_client_id ||
                  !event.secrets.personapi_client_secret ||
                  !event.secrets.personapi_url ||
                  !event.secrets.personapi_audience) {
          -     logger.error('Error: Unable to find secrets');
          +     console.log('Error: Unable to find secrets');
                return;
              }
            
              // Retrieve and return a secret from AWS Secrets Manager
              const getSecrets = async (AWS, accessKeyId, secretAccessKey) => {
                try {
            
                  if (!accessKeyId || !secretAccessKey) {
                    throw new Error('AWS access keys are not defined.');
                  }
            
                  // set AWS config so we can retrieve secrets
                  AWS.config.update({
                    region: 'us-west-2',
                    accessKeyId: accessKeyId,
                    secretAccessKey: secretAccessKey
                  });
            
                  const secretsManager = new AWS.SecretsManager();
                  const secretPath = event.tenant.id === "dev" ? "/iam/auth0/dev/actions" : "/iam/auth0/prod/actions";
                  const data = await secretsManager.getSecretValue({ SecretId: secretPath }).promise();
                  // handle string or binary
                  if ('SecretString' in data) {
                    return JSON.parse(data.SecretString);
                  } else {
                    let buff = Buffer.from(data.SecretBinary, 'base64');
                    return buff.toString('ascii');
                  }
                } catch (err) {
          -       logger.error("getSecrets:" + err);
          +       console.log("getSecrets:", err);
                  throw err;
                };
              }
            
              // Load secrets
              const accessKeyId = event.secrets.accessKeyId;
              const secretAccessKey = event.secrets.secretAccessKey;
              const secrets = await getSecrets(AWS, accessKeyId, secretAccessKey);
              const changeapi_auth0_private_key = secrets.changeapi_auth0_private_key
              const changeapi_null_profile = secrets.changeapi_null_profile;
            
              // we also need to decode the private key from base64 into a PEM format that `jsonwebtoken` understands
              // generated with:
              // import base64
              // import boto3
              // base64.b64encode(boto3.client('ssm').get_parameter(Name='/iam/cis/development/keys/access_provider', WithDecryption=True)['Parameter']['Value'].encode('ascii')).decode('ascii')
              const privateKey = Buffer.from(changeapi_auth0_private_key, 'base64').toString('ascii');
            
              const getBearerToken = async () => {
          -     logger.info('Retrieving bearer token to create new user in CIS');
          +     console.log('Retrieving bearer token to create new user in CIS');
            
                const options = {
                  method: 'POST',
                  headers: {
                    'Content-Type': 'application/json',
                  },
                  timeout: AUTH0_TIMEOUT,
                  body: JSON.stringify({
                    audience: event.secrets.personapi_audience,
                    client_id: event.secrets.personapi_client_id,
                    client_secret: event.secrets.personapi_client_secret,
                    grant_type: 'client_credentials',
                  })
                };
            
                try {
                  const response = await fetch(event.secrets.personapi_oauth_url, options);
                  if (!response.ok) {
                    // Throw an error if the response status code is not in the 200-299 range
                    throw new Error(`HTTP error! status: ${response.status}`);
                  }
                  const data = await response.json();
                  // Cache bearer token, so it's not constantly retrieved
                  // TODO: actually cache the bearer token
                  const personapi_bearer_token = data.access_token;
          -       logger.info(`Successfully retrieved bearer token from Auth0`);
          +       console.log(`Successfully retrieved bearer token from Auth0`);
                  return personapi_bearer_token;
                } catch (err) {
                  throw Error(`Unable to retrieve bearer token from Auth0: ${err}`);
                }
              };
            
              const createPersonProfile = () => {
          -     logger.info(`Generating CIS profile for ${USER_ID}`);
          +     console.log(`Generating CIS profile for ${USER_ID}`);
            
                const date = new Date();
                const now = date.toISOString();
            
                // load the user skeleton, as generated by:
                // base64.b64encode(json.dumps(requests.get('https://raw.githubusercontent.com/mozilla-iam/cis/master/python-modules/cis_profile/cis_profile/data/user_profile_null.json').json(), separators=(',', ':')).encode('ascii'))
                const profile = JSON.parse(Buffer.from(changeapi_null_profile, 'base64').toString('ascii'));
            
                // update attributes in the skeleton
                // normally we shouldn't need to change anything but the values, but this is manually doing it because
                // I have no idea if the skeleton will ever change underneath me
                profile.active.metadata.last_modified = now;
                profile.active.signature.publisher.name = PUBLISHER_NAME;
                profile.active.value = true;
            
                // order goes given_name -> name -> family_name -> nickname -> ' '
                profile.first_name.metadata.display = 'private';
                profile.first_name.metadata.last_modified = now;
                profile.first_name.signature.publisher.name = PUBLISHER_NAME;
                profile.first_name.value = event.user.given_name || event.user.name || event.user.family_name || event.user.nickname || ' ';
            
                profile.last_name.metadata.display = 'private';
                profile.last_name.metadata.last_modified = now;
                profile.last_name.signature.publisher.name = PUBLISHER_NAME;
                profile.last_name.value = event.user.family_name ? event.user.family_name : ' ';
            
                profile.primary_email.metadata.last_modified = now;
                profile.primary_email.signature.publisher.name = PUBLISHER_NAME;
                profile.primary_email.value = event.user.email;
            
                profile.user_id.metadata.last_modified = now;
                profile.user_id.signature.publisher.name = PUBLISHER_NAME;
                profile.user_id.value = USER_ID;
            
                // now we need to go and update the identities values; this is based on the logic here:
                // https://github.com/mozilla-iam/cis/blob/master/python-modules/cis_publisher/cis_publisher/auth0.py
                // which may or may not be correct, I dunno
                for (let i = 0; i < event.user.identities.length; i++) {
                  const identity = event.user.identities[i];
                  // ignore a provider if it's not whitelisted
                  if (!WHITELISTED_CONNECTIONS.includes(identity.connection)) {
                    continue;
                  }
            
                  // store the login_method for the first identity
                  if (i === 0) {
                    profile.login_method.metadata.last_modified = now;
                    profile.login_method.signature.publisher.name = PUBLISHER_NAME;
                    profile.login_method.value = identity.connection;
                    if (identity.provider === 'ad' && (identity.connection === 'Mozilla-LDAP' || identity.connection === 'Mozilla-LDAP-Dev')) {
                      profile.first_name.metadata.display = 'staff';
                      profile.last_name.metadata.display = 'staff';
                      profile.primary_email.metadata.display = 'staff';
                      // Note : This user will not show up as a staff member in people.mozilla.org
                      // until the LDAP publisher runs and updates their CiS profile (which is being
                      // created here) to have the "hris" data structure. That is what let's
                      // people.mozilla.org know that the user is a staff member.
                    }
                  }
            
                  if (identity.provider === 'github') {
                    profile.identities.github_id_v3.metadata.display = 'private';
                    profile.identities.github_id_v3.metadata.last_modified = now;
                    profile.identities.github_id_v3.signature.publisher.name = PUBLISHER_NAME;
                    profile.identities.github_id_v3.value = identity.user_id;
            
                    if (event.user.nickname) {
                      profile.usernames.metadata.display = 'private';
                      profile.usernames.signature.publisher.name = PUBLISHER_NAME;
                      profile.usernames.values = {"HACK#GITHUB": event.user.nickname};
                    }
            
                    if (identity.profileData) {
                      // I could never seem to find a user that met this condition
                      profile.identities.github_id_v4.metadata.display = 'private';
                      profile.identities.github_id_v4.metadata.last_modified = now;
                      profile.identities.github_id_v4.signature.publisher.name = PUBLISHER_NAME;
                      profile.identities.github_id_v4.value = identity.profileData.node_id;
            
                      profile.identities.github_primary_email.metadata.display = 'private';
                      profile.identities.github_primary_email.metadata.last_modified = now;
                      profile.identities.github_primary_email.metadata.verified = identity.profileData.email_verified === true;
                      profile.identities.github_primary_email.signature.publisher.name = PUBLISHER_NAME;
                      profile.identities.github_primary_email.value = identity.profileData.email;
                    }
                  }
            
                  else if (identity.provider === 'google-oauth2') {
                    profile.identities.google_oauth2_id.metadata.display = 'private';
                    profile.identities.google_oauth2_id.metadata.last_modified = now;
                    profile.identities.google_oauth2_id.signature.publisher.name = PUBLISHER_NAME;
                    profile.identities.google_oauth2_id.value = identity.user_id;
            
                    profile.identities.google_primary_email.metadata.display = 'private';
                    profile.identities.google_primary_email.metadata.last_modified = now;
                    profile.identities.google_primary_email.signature.publisher.name = PUBLISHER_NAME;
                    profile.identities.google_primary_email.value = event.user.email;
                  }
            
                  else if (identity.connection === 'firefoxaccounts' && identity.provider === 'oauth2') {
                    profile.identities.firefox_accounts_id.metadata.display = 'private';
                    profile.identities.firefox_accounts_id.metadata.last_modified = now;
                    profile.identities.firefox_accounts_id.signature.publisher.name = PUBLISHER_NAME;
                    profile.identities.firefox_accounts_id.value = identity.user_id;
            
                    profile.identities.firefox_accounts_primary_email.metadata.display = 'private';
                    profile.identities.firefox_accounts_primary_email.metadata.last_modified = now;
                    profile.identities.firefox_accounts_primary_email.signature.publisher.name = PUBLISHER_NAME;
                    profile.identities.firefox_accounts_primary_email.value = event.user.email;
                  }
            
                  else if (identity.provider === 'ad' && (identity.connection === 'Mozilla-LDAP' || identity.connection === 'Mozilla-LDAP-Dev')) {
                    // Auth0 gets LDAP attributes from the Auth0 LDAP Connector.
                    // We've patched the LDAP connector to pass addition LDAP fields
                    // https://github.com/mozilla-iam/ad-ldap-connector-rpm/tree/master/patches
            
                    // The Auth0 publisher can't currently publish this attribute as it's not
                    // permitted to : https://auth.mozilla.com/.well-known/mozilla-iam-publisher-rules
                    // If these publisher rules were to change this could be published by the Auth0
                    // publisher. Until then, this value won't be correct until the LDAP publisher
                    // updates it.
                    // profile.identities.mozilla_ldap_primary_email = user.email;
            
                    // The following fields were previously published by the LDAP publisher
                    // when it was tasked with creating new CIS profiles for LDAP users
                    // They appear to not be available to this rule as they aren't in the
                    // user object and aren't passed by the LDAP connector
                    // profile.identities.mozilla_ldap_id = '[email protected],o=com,dc=mozilla';
                    // profile.identities.mozilla_posix_id = 'jdoe';
                    // profile.identities.mozilliansorg_id = null;
                  }
                }
            
                // now, we need to sign every field and subfield
                signAll(profile);
            
                // turn this on only for debugging
          -     // logger.info(`Generated profile:\n${JSON.stringify(profile, null, 2)}`);
          +     // console.log(`Generated profile:\n${JSON.stringify(profile, null, 2)}`);
                return profile;
              };
            
              const getPersonProfile = async (bearerToken) => {
                const options = {
                  method: 'GET',
                  headers: {
                    'Authorization': `Bearer ${bearerToken}`,
                  },
                  timeout: PERSONAPI_TIMEOUT,
                };
                const url = `${event.secrets.personapi_url}/v2/user/user_id/${encodeURI(USER_ID)}?active=any`;
            
          -     logger.info(`Fetching person profile of ${USER_ID}`);
          +     console.log(`Fetching person profile of ${USER_ID}`);
            
                try {
                  const response = await fetch(url, options);
                  if (!response.ok) {
                    // Throw an error if the response status code is not in the 200-299 range
                    throw new Error(`HTTP error! status: ${response.status}`);
                  }
                  return await response.json();
                } catch (error) {
                  throw Error(`Unable to retrieve profile from Person API: ${error}`);
                }
              };
            
              const postProfile = async (bearerToken, profile) => {
          -     logger.info(`Posting profile for ${USER_ID} to ChangeAPI`);
          +     console.log(`Posting profile for ${USER_ID} to ChangeAPI`);
            
                const options = {
                  method: 'POST',
                  headers: {
                    'Authorization': `Bearer ${bearerToken}`,
                    'Content-Type': 'application/json',
                  },
                  body: JSON.stringify(profile),
                  timeout: CHANGEAPI_TIMEOUT,
                };
                const url = `${event.secrets.changeapi_url}/v2/user?user_id=${encodeURI(USER_ID)}`;
            
                // POST the profile to the ChangeAPI
                try {
                  const response = await fetch(url, options);
                  if (!response.ok) {
                    // Throw an error if the response status code is not in the 200-299 range
                    throw new Error(`HTTP error! status: ${response.status}`);
                  }
          -       logger.info(`Successfully created profile for ${event.user.user_id} in ChangeAPI as ${USER_ID}`);
          +       console.log(`Successfully created profile for ${event.user.user_id} in ChangeAPI as ${USER_ID}`);
                  // set their profile as existing in CIS
                  setExistsInCIS();
                } catch (error) {
                  throw Error(`Unable to create profile for ${USER_ID} in ChangeAPI: ${error}`);
                }
              };
            
              const signAll = profile => {
                // now we need to sign every attribute in the profile, if we're allowed to
                // otherwise, we will descend one level deep for sub attributes
                // this is super hacky and ugly and I hate it
                Object.values(profile).forEach(attr => {  // works because profile is mutable
                  if (attr.constructor === Object && attr.signature) {
                    signAttribute(attr);
                  } else if (attr.constructor === Object && !attr.signature) {
                    signAll(attr);  // descend deeper
                  }
                });
              };
            
              const signAttribute = attr => {
            
                // we can only sign attributes that access_provider (e.g. auth0) is allowed to sign
                // we also ignore things that don't have a pre-existing signature field
                // we also don't need to sign null attributes
                if (!attr.signature || attr.signature.publisher.name !== PUBLISHER_NAME || attr.value === null || attr.values === null) {
                  return attr;
                }
            
                // this is an ugly hack, as the CIS profile currently requires all integers to be cast into strings
                if (attr.value && typeof attr.value === "number") {
                  attr.value = attr.value.toString();
                }
            
                // we need to delete the existing signature and generate it anew
                delete(attr.signature);
            
                attr.signature = {
                  additional: [{
                    alg: 'RS256',
                    name: null,
                    typ: 'JWS',
                    value: '',
                  }],
                  publisher: {
                    alg: 'RS256',
                    name: PUBLISHER_NAME,
                    typ: 'JWS',
                    value: jwt.sign(attr, privateKey, { algorithm: 'RS256', noTimestamp: true }),
                  }
                };
            
                return attr;
              };
            
              const setExistsInCIS = () => {
                // update user metadata to store them existing
                api.user.setAppMetadata("existsInCIS", true);
          -     logger.info(`Updated user metadata on ${event.user.user_id} to set existsInCIS`);
          +     console.log(`Updated user metadata on ${event.user.user_id} to set existsInCIS`);
              };
            
              // if we get this far, we need to 1) call the PersonAPI to check for existance, and 2) if the user
              // doesn't exist, call the ChangeAPI to create them
              try {
                // Get a bearer token for accessing both PersonAPI and ChangeAPI
                const bearerToken = await getBearerToken();
                // Read the profile, if it exists
                const profile = await getPersonProfile(bearerToken);
            
                // If the profile already exists, set the existsInCIS in app.metadata
                if (Object.keys(profile).length !== 0) {
                  setExistsInCIS();
          -       logger.info(`Profile for ${event.user.user_id} already exists in PersonAPI as ${USER_ID}`);
          +       console.log(`Profile for ${event.user.user_id} already exists in PersonAPI as ${USER_ID}`);
            
                  return;
                }
            
                // Generate a blank profile and populate it with this users data
                const newProfile = createPersonProfile();
            
                // Submit the newly created profile to ChangeAPI
                await postProfile(bearerToken, newProfile);
            
                return;
              } catch (err) {
                // Catch error, log it and continue on with workflow
          -     logger.error(err);
          +     console.error(err);
                return;
              }
            }
        EOT
        id         = "0bf6c54b-100f-4732-95c6-9c96e237600d"
        name       = "activateNewUsersInCIS"
        # (3 unchanged attributes hidden)

      - dependencies {
          - name    = "@google-cloud/logging-winston" -> null
          - version = "6.0.0" -> null
        }
      - dependencies {
          - name    = "winston" -> null
          - version = "3.17.0" -> null
        }
      + dependencies {
          + name    = "node-fetch"
          + version = "2.7.0"
        }

      - secrets {
          - name  = "google_service_account_creds" -> null
          - value = (sensitive value) -> null
        }

        # (11 unchanged blocks hidden)
    }

  # auth0_action.awsSaml will be updated in-place
  ~ resource "auth0_action" "awsSaml" {
      ~ code       = <<-EOT
          - const { LoggingWinston } = require('@google-cloud/logging-winston');
          - const winston = require('winston');
            const AWS = require("aws-sdk");
            
            exports.onExecutePostLogin = async (event, api) => {
          +   console.log("Running actions:", "awsSaml");
            
          -   const serviceAccountCreds = JSON.parse(event.secrets.google_service_account_creds);
          - 
          -   // Create a GCP LoggingWinston transport with credentials
          -   const loggingWinston = new LoggingWinston({
          -     projectId: 'iam-auth0',
          -     logName: `auth0-action-${event.tenant.id}`,
          -     credentials: serviceAccountCreds,
          -     labels: {
          -       tenant: event.tenant.id || null,
          -       client_id: event.client.client_id || null,
          -       client_name: event.client.name || null,
          -       connection_id: event.connection.id || null,
          -       connection_name: event.connection.name || null,
          -       connection_strategy: event.connection.strategy || null,
          -       session_id: event.session.id || null,
          -       user_id: event.user.user_id || null,
          -       user_email: event.user.email || null,
          -       service: 'auth0-actions',
          -     },
          -   });
          - 
          -   // Create the Winston logger
          -   const logger = winston.createLogger({
          -     level: 'info', // Set the minimum log level
          -     transports: [
          -       // Add Google Cloud Logging transport
          -       loggingWinston,
          - 
          -       // Also log to the console
          -       new winston.transports.Console({
          -         format: winston.format.combine(
          -           winston.format.colorize(),
          -           winston.format.simple()
          -         ),
          -       }),
          -     ],
          -   });
          - 
          -   logger.info("Running actions: awsSaml");
          - 
              // Only continue on auth0 prod tenant
              if (event.tenant.id !== "auth") {
          -     logger.info(`Skipping awsSAML; tenant is ${event.tenant.id}`);
          +     console.log(`Skipping awsSAML; tenant is ${event.tenant.id}`);
                return;
              }
            
              var paramObj = {};
            
              const clientID = event.client.client_id || "";
              switch (clientID) {
                case "JR8HkyiM2i00ma2d1X2xfgdbEHzEYZbS":
                  // IT billing account params
                  paramObj.region = "us-west-2";
                  paramObj.IdentityStoreId = event.secrets.AWS_IDENTITYSTORE_ID_IT;
                  paramObj.accessKeyId = event.secrets.AWS_IDENTITYSTORE_ACCESS_ID_IT;
                  paramObj.secretAccessKey = event.secrets.AWS_IDENTITYSTORE_ACCESS_KEY_IT;
                  paramObj.awsGroups = [
                    "fuzzing_team",
                    "mozilliansorg_aws_billing_access",
                    "mozilliansorg_cia-aws",
                    "mozilliansorg_consolidated-billing-aws",
                    "mozilliansorg_consolidated-billing-aws-readonly",
                    "mozilliansorg_discourse-devs",
                    "mozilliansorg_http-observatory-rds",
                    "mozilliansorg_iam-admins",
                    "mozilliansorg_iam-in-transition",
                    "mozilliansorg_iam-in-transition-admin",
                    "mozilliansorg_iam-readonly",
                    "mozilliansorg_meao-admins",
                    "mozilliansorg_mozilla-moderator-devs",
                    "mozilliansorg_partinfra-aws",
                    "mozilliansorg_pdfjs-testers",
                    "mozilliansorg_pocket_cloudtrail_readers",
                    "mozilliansorg_project-guardian-admins",
                    "mozilliansorg_relay_developer",
                    "mozilliansorg_searchfox-aws",
                    "mozilliansorg_secops-aws-admins",
                    "mozilliansorg_sre",
                    "mozilliansorg_sumo-admins",
                    "mozilliansorg_sumo-devs",
                    "mozilliansorg_voice_aws_admin_access",
                    "mozilliansorg_web-sre-aws-access",
                    "mozilliansorg_webcompat-alexa-admins",
                    "team_mdn",
                    "team_netops",
                    "team_opsec",
                    "team_se",
                    "team_secops",
                    "voice-dev",
                    "vpn_sumo_aws_devs"
                  ];
                  break;
                case "pQ0eb5tzwfYHnAtzGuk88pzxZ68szQtk":
                  // Pocket Billing Account
                  paramObj.region = "us-east-1";
                  paramObj.IdentityStoreId = event.secrets.AWS_IDENTITYSTORE_ID_POCKET;
                  paramObj.accessKeyId = event.secrets.AWS_IDENTITYSTORE_ACCESS_ID_POCKET;
                  paramObj.secretAccessKey =
                    event.secrets.AWS_IDENTITYSTORE_ACCESS_KEY_POCKET;
                  paramObj.awsGroups = [
                    "mozilliansorg_pocket_admin",
                    "mozilliansorg_pocket_backend",
                    "mozilliansorg_pocket_backup_admin",
                    "mozilliansorg_pocket_backup_readonly",
                    "mozilliansorg_pocket_cloudtrail_readers",
                    "mozilliansorg_pocket_dataanalytics",
                    "mozilliansorg_pocket_datalearning",
                    "mozilliansorg_pocket_developer",
                    "mozilliansorg_pocket_fin_ops",
                    "mozilliansorg_pocket_frontend",
                    "mozilliansorg_pocket_marketing",
                    "mozilliansorg_pocket_mozilla_sre",
                    "mozilliansorg_pocket_qa",
                    "mozilliansorg_pocket_readonly",
                    "mozilliansorg_pocket_sales",
                    "mozilliansorg_pocket_ads",
                    "mozilliansorg_pocket_aws_billing",
                    "mozilliansorg_infrasec"
                  ];
                  break;
                case "jU8r4uSEF3fUCjuJ63s46dBnHAfYMYfj":
                  // MoFo Billing Account
                  paramObj.region = "us-east-2";
                  paramObj.IdentityStoreId = event.secrets.AWS_IDENTITYSTORE_ID_MOFO;
                  paramObj.accessKeyId = event.secrets.AWS_IDENTITYSTORE_ACCESS_ID_MOFO;
                  paramObj.secretAccessKey =
                    event.secrets.AWS_IDENTITYSTORE_ACCESS_KEY_MOFO;
                  paramObj.awsGroups = [
                    "mozilliansorg_mofo_aws_admins",
                    "mozilliansorg_mofo_aws_community",
                    "mozilliansorg_mofo_aws_everything",
                    "mozilliansorg_mofo_aws_labs",
                    "mozilliansorg_mofo_aws_projects",
                    "mozilliansorg_mofo_aws_sandbox",
                    "mozilliansorg_mofo_aws_secure",
                    "mozilliansorg_infrasec"
                  ];
                  break;
                case "c0x6EoLdp55H2g2OXZTIUuaQ4v8U4xf9":
                  // CloudServices billing account params
                  paramObj.region = "us-west-2";
                  paramObj.IdentityStoreId = event.secrets.AWS_IDENTITYSTORE_ID_CLOUDSERVICES;
                  paramObj.accessKeyId = event.secrets.AWS_IDENTITYSTORE_ACCESS_ID_CLOUDSERVICES;
                  paramObj.secretAccessKey = event.secrets.AWS_IDENTITYSTORE_ACCESS_KEY_CLOUDSERVICES;
                  paramObj.awsGroups = [
                    "mozilliansorg_aws_billing_access",
                    "mozilliansorg_cloudservices_aws_admin",
                    "mozilliansorg_cloudservices_aws_autograph_admin",
                    "mozilliansorg_cloudservices_aws_autograph_dev",
                    "mozilliansorg_cloudservices_aws_dev_developers",
                    "mozilliansorg_cloudservices_aws_developer_services_dev",
                    "mozilliansorg_cloudservices_aws_fxa_developers",
          +         "mozilliansorg_cloudservices_aws_taskcluster_devs",
                    "mozilliansorg_infrasec"
                  ];
                  break;
                default:
                  return; // Not an AWS login, continue auth pipeline
              }
            
              // Instantate and set Region
              var i = new AWS.IdentityStore({
                region: paramObj.region,
                apiVersion: "2020-06-15",
                accessKeyId: paramObj.accessKeyId,
                secretAccessKey: paramObj.secretAccessKey,
              });
            
              const IdentityStoreId = paramObj.IdentityStoreId;
              const userName = event.user.email;
              var AWSUserId = "";
            
              // This is a list of groups that are mapped to AWS groups
              const AWS_GROUPS = paramObj.awsGroups;
            
              // Filter the users Auth0 groups down to only those mapped to AWS groups
              function filterAWSGroups(groups) {
                var filteredGroups = groups.filter((x) => AWS_GROUPS.includes(x));
                return filteredGroups;
              }
            
              function userAuth0Groups(proposedGroups, existingGroups) {
                var addToGroup = proposedGroups.filter((x) => !existingGroups.includes(x));
                var removeFromGroup = existingGroups.filter(
                  (x) => !proposedGroups.includes(x)
                );
                return { addToGroup: addToGroup, removeFromGroup: removeFromGroup };
              }
            
              function createGroupMemberships(addToGroup) {
                var creationPromises = [];
                for (var groupId of addToGroup) {
                  var params = {
                    IdentityStoreId: IdentityStoreId,
                    GroupId: groupId,
                    MemberId: {
                      UserId: AWSUserId,
                    },
                  };
                  creationPromises.push(i.createGroupMembership(params).promise());
                }
                return Promise.all(creationPromises);
              }
            
              function removeGroupMemberships(removeMembershipId) {
                var removalPromises = [];
                for (var membershipId of removeMembershipId) {
                  var params = {
                    IdentityStoreId: IdentityStoreId,
                    MembershipId: membershipId,
                  };
                  removalPromises.push(i.deleteGroupMembership(params).promise());
                }
                return Promise.all(removalPromises);
              }
            
              function fetchAWSUUID() {
                var params = {
                  Filters: [
                    {
                      AttributePath: "UserName",
                      AttributeValue: userName,
                    },
                  ],
                  IdentityStoreId: IdentityStoreId,
                };
                var userId = i.listUsers(params).promise();
                return userId; // returns promise
              }
            
              function fetchUsersAWSGroups(userUUID) {
                var params = {
                  IdentityStoreId: IdentityStoreId,
                  MemberId: {
                    UserId: userUUID,
                  },
                  MaxResults: 50,
                };
                // TODO: handle pagenation!!!
                var userMembership = i.listGroupMembershipsForMember(params).promise();
                return userMembership;
              }
            
              function fetchGroupNameMap(groupList) {
                var groupPromises = [];
                for (var group of groupList) {
                  var params = {
                    GroupId: group.GroupId,
                    IdentityStoreId: IdentityStoreId,
                  };
                  groupPromises.push(i.describeGroup(params).promise());
                }
                return Promise.all(groupPromises);
              }
            
              function getGroupIds(groupList) {
                var promisedGroupIds = [];
                for (var groupName of groupList) {
                  var params = {
                    IdentityStoreId: IdentityStoreId,
                    AlternateIdentifier: {
                      UniqueAttribute: {
                        AttributePath: "DisplayName",
                        AttributeValue: groupName,
                      },
                    },
                  };
                  promisedGroupIds.push(i.getGroupId(params).promise());
                }
                return Promise.all(promisedGroupIds);
              }
            
              function createUser() {
                var params = {
                  IdentityStoreId: IdentityStoreId,
                  DisplayName: event.user.name,
                  UserName: event.user.email,
                  Name: {
                    FamilyName: event.user.family_name,
                    GivenName: event.user.given_name,
                  },
                  Emails: [
                    {
                      Primary: true,
                      Value: event.user.email,
                    },
                  ],
                };
                return i.createUser(params).promise();
              }
            
              // Main
              try {
                // Get the users group list filtered down to only AWS related groups
                const proposedGroups = filterAWSGroups(event.user.groups);
            
                // Fetch users AWS UUID
                const userObjList = await fetchAWSUUID();
                if (userObjList.Users.length === 0) {
          -       logger.info(
          +       console.log(
                    `[${IdentityStoreId}] Creating User (${userName}) in AWS IdentityStore`
                  );
                  AWSUserId = (await createUser()).UserId;
                } else {
                  AWSUserId = userObjList.Users[0].UserId;
                }
            
                // Get users existing AWS group membership
                const usersAWSGroups = await fetchUsersAWSGroups(AWSUserId);
            
                const usersAWSGroupNames = await fetchGroupNameMap(
                  usersAWSGroups.GroupMemberships
                );
                const existingGroups = usersAWSGroupNames.map((item) => item.DisplayName);
            
                // Diff the proposed groups and the existing groups
                const groupActionList = userAuth0Groups(proposedGroups, existingGroups);
                const addToGroup = groupActionList.addToGroup; // DisplayName list
                const removeFromGroup = groupActionList.removeFromGroup; // DisplayName list
            
                if (addToGroup.length > 0 || removeFromGroup.length > 0) {
          -       logger.info(
          +       console.log(
                    `[${IdentityStoreId}] Add user (${userName}) to: `,
                    addToGroup
                  );
          -       logger.info(
          +       console.log(
                    `[${IdentityStoreId}] Remove user (${userName}) from: `,
                    removeFromGroup
                  );
            
                  const addToGroupIds = (await getGroupIds(addToGroup)).map(
                    (item) => item.GroupId
                  );
            
                  // From the groupsmembership object, filter and map group ids to be removed from
                  const removeGroupIds = usersAWSGroupNames
                    .filter((item) => removeFromGroup.includes(item.DisplayName))
                    .map((item) => item.GroupId);
                  const removeMembershipId = usersAWSGroups.GroupMemberships.filter(
                    (item) => removeGroupIds.includes(item.GroupId)
                  ).map((item) => item.MembershipId);
            
                  // Create group memberships
                  const addPromise = createGroupMemberships(addToGroupIds);
            
                  // Delete group memberships
                  const removePromise = removeGroupMemberships(removeMembershipId);
                  return Promise.all([addPromise, removePromise]);
                }
            
                return;
              } catch (err) {
          -     logger.error(err);
          +     console.error(err);
                return api.access.deny(err);
              }
            }
        EOT
        id         = "30b9c0ac-ad36-4244-a586-c64847eac099"
        name       = "awsSaml"
        # (3 unchanged attributes hidden)

      - dependencies {
          - name    = "@google-cloud/logging-winston" -> null
          - version = "6.0.0" -> null
        }
      - dependencies {
          - name    = "winston" -> null
          - version = "3.17.0" -> null
        }

      - secrets {
          - name  = "google_service_account_creds" -> null
          - value = (sensitive value) -> null
        }

        # (14 unchanged blocks hidden)
    }

  # auth0_action.continueEndPoint will be updated in-place
  ~ resource "auth0_action" "continueEndPoint" {
      ~ code       = <<-EOT
          - const { LoggingWinston } = require('@google-cloud/logging-winston');
          - const winston = require('winston');
          - 
            exports.onExecutePostLogin = async (event, api) => {return};
            
            exports.onContinuePostLogin = async (event, api) => {
          - 
          -   const serviceAccountCreds = JSON.parse(event.secrets.google_service_account_creds);
          - 
          -   // Create a GCP LoggingWinston transport with credentials
          -   const loggingWinston = new LoggingWinston({
          -     projectId: 'iam-auth0',
          -     logName: `auth0-action-${event.tenant.id}`,
          -     credentials: serviceAccountCreds,
          -     labels: {
          -       tenant: event.tenant.id || null,
          -       client_id: event.client.client_id || null,
          -       client_name: event.client.name || null,
          -       connection_id: event.connection.id || null,
          -       connection_name: event.connection.name || null,
          -       connection_strategy: event.connection.strategy || null,
          -       session_id: event.session.id || null,
          -       user_id: event.user.user_id || null,
          -       user_email: event.user.email || null,
          -       service: 'auth0-actions',
          -     },
          -   });
          - 
          -   // Create the Winston logger
          -   const logger = winston.createLogger({
          -     level: 'info', // Set the minimum log level
          -     transports: [
          -       // Add Google Cloud Logging transport
          -       loggingWinston,
          - 
          -       // Also log to the console
          -       new winston.transports.Console({
          -         format: winston.format.combine(
          -           winston.format.colorize(),
          -           winston.format.simple()
          -         ),
          -       }),
          -     ],
          -   });
          - 
              // Since we do not use the /continue endpoint let's make sure we explictly fail with an UnauthorizedError
              // otherwise it is possible to continue the session even after a postError redirect is set.
          -   const msg = "The /continue endpoint is not allowed"
          -   logger.info(`Access Denied: ${msg}`);
          -   return api.access.deny(msg);
          +   return api.access.deny('The /continue endpoint is not allowed');
            };
        EOT
        id         = "f897f8f1-179c-4e52-b19a-be020795fe46"
        name       = "continueEndPoint"
        # (3 unchanged attributes hidden)

      - dependencies {
          - name    = "@google-cloud/logging-winston" -> null
          - version = "6.0.0" -> null
        }
      - dependencies {
          - name    = "winston" -> null
          - version = "3.17.0" -> null
        }

      - secrets {
          - name  = "google_service_account_creds" -> null
          - value = (sensitive value) -> null
        }

        # (1 unchanged block hidden)
    }

  # auth0_action.ensureLdapUsersUseLdap will be updated in-place
  ~ resource "auth0_action" "ensureLdapUsersUseLdap" {
      ~ code       = <<-EOT
            // Required Libraries
          - const { LoggingWinston } = require('@google-cloud/logging-winston');
          - const winston = require('winston');
            const jwt = require('jsonwebtoken');
            const AWS = require('aws-sdk');
            
            exports.onExecutePostLogin = async (event, api) => {
          +   console.log("Running actions:", "ensureLdapLoginsUseLdap");
            
          -   const serviceAccountCreds = JSON.parse(event.secrets.google_service_account_creds);
          - 
          -   // Create a GCP LoggingWinston transport with credentials
          -   const loggingWinston = new LoggingWinston({
          -     projectId: 'iam-auth0',
          -     logName: `auth0-action-${event.tenant.id}`,
          -     credentials: serviceAccountCreds,
          -     labels: {
          -       tenant: event.tenant.id || null,
          -       client_id: event.client.client_id || null,
          -       client_name: event.client.name || null,
          -       connection_id: event.connection.id || null,
          -       connection_name: event.connection.name || null,
          -       connection_strategy: event.connection.strategy || null,
          -       session_id: event.session.id || null,
          -       user_id: event.user.user_id || null,
          -       user_email: event.user.email || null,
          -       service: 'auth0-actions',
          -     },
          -   });
          - 
          -   // Create the Winston logger
          -   const logger = winston.createLogger({
          -     level: 'info', // Set the minimum log level
          -     transports: [
          -       // Add Google Cloud Logging transport
          -       loggingWinston,
          - 
          -       // Also log to the console
          -       new winston.transports.Console({
          -         format: winston.format.combine(
          -           winston.format.colorize(),
          -           winston.format.simple()
          -         ),
          -       }),
          -     ],
          -   });
          - 
          -   logger.info("Running actions: ensureLdapLoginsUseLdap");
          - 
              // Retrieve and return a secret from AWS Secrets Manager
              const getSecrets = async (AWS, accessKeyId, secretAccessKey) => {
                try {
            
                  if (!accessKeyId || !secretAccessKey) {
                    throw new Error('AWS access keys are not defined.');
                  }
            
                  // set AWS config so we can retrieve secrets
                  AWS.config.update({
                    region: 'us-west-2',
                    accessKeyId: accessKeyId,
                    secretAccessKey: secretAccessKey
                  });
            
                  const secretsManager = new AWS.SecretsManager();
                  const secretPath = event.tenant.id === "dev" ? "/iam/auth0/dev/actions" : "/iam/auth0/prod/actions";
                  const data = await secretsManager.getSecretValue({ SecretId: secretPath }).promise();
                  // handle string or binary
                  if ('SecretString' in data) {
                    return JSON.parse(data.SecretString);
                  } else {
                    let buff = Buffer.from(data.SecretBinary, 'base64');
                    return buff.toString('ascii');
                  }
                } catch (err) {
          -       logger.info("getSecrets:" + err);
          +       console.log("getSecrets:", err);
                  throw err;
                };
              }
            
              const accessKeyId = event.secrets.accessKeyId;
              const secretAccessKey = event.secrets.secretAccessKey;
              const secrets = await getSecrets(AWS, accessKeyId, secretAccessKey);
              const jwtMsgsRsaSkey = secrets.jwtMsgsRsaSkey;
            
              // postError(code)
              // @code string with an error code for the SSO Dashboard to display
              // Returns rcontext with redirect set to the error
              const postError = (code, prefered_connection_arg) => {
                try {
                  const prefered_connection = prefered_connection_arg || ""; // Optional arg
                  if (!jwtMsgsRsaSkey) {
                    throw new Error('jwtMsgsRsaSkey is not defined.');
                  }
                  // Token is valid from 30s ago, to 1h from now
                  const skey = Buffer.from(jwtMsgsRsaSkey, 'base64').toString('ascii');
                  const token = jwt.sign(
                    {
                      client: event.client.name,
                      code: code,
                      connection: event.connection.name,
                      exp: Math.floor(Date.now() / 1000) + 3600,
                      iat: Math.floor(Date.now() / 1000) - 30,
                      preferred_connection_name: prefered_connection,
                      redirect_uri: event.transaction.redirect_uri,
                    },
                    skey,
                    { algorithm: 'RS256' }
                  );
            
                  const domain = event.tenant.id === "dev" ? "sso.allizom.org" : "sso.mozilla.com";
                  const forbiddenUrl = new URL(`https://${domain}/forbidden`);
                  forbiddenUrl.searchParams.set("error", token);
                  api.redirect.sendUserTo(forbiddenUrl.href);
            
                  return;
                } catch (err) {
          -       logger.info("postError:" + err);
          +       console.log("postError:", err);
                  throw err;
                }
              }
            
              const WHITELIST = [
                'HvN5D3R64YNNhvcHKuMKny1O0KJZOOwH',  // mozillians.org account verification
                't9bMi4eTCPpMp5Y6E1Lu92iVcqU0r1P1',  // https://web-mozillians-staging.production.paas.mozilla.community Verification client
                'jijaIzcZmFCDRtV74scMb9lI87MtYNTA',  // mozillians.org Verification Client
              ];
            
              // The domain strings in this array should always be declared here in lowercase
              const MOZILLA_STAFF_DOMAINS = [
                'mozilla.com',            // Main corp domain
                'mozillafoundation.org',  // Main org domain
                'getpocket.com',          // Pocket domain
                'thunderbird.net',        // MZLA domain
                'readitlater.com',
                'mozilla-japan.org',
                'mozilla.ai',
                'mozilla.vc'
              ];
            
              // Sanity checks
              if (!event.user.email_verified) {
          -     logger.error(`Primary email not verified, can't let the user in. This should not happen.`);
          +     console.log(`Primary email not verified, can't let the user in. This should not happen.`);
                postError('primarynotverified');
              }
            
              // Ignore whitelisted clients
              if (WHITELIST.includes(event.client.client_id)) {
          -     logger.info(`Whitelisted client ${event.client.client_id}, no login enforcement taking place`);
          +     console.log(`Whitelisted client ${event.client.client_id}, no login enforcement taking place`);
                return;
              }
            
              // 'ad' is LDAP - Force LDAP users to log with LDAP here
              if (event.connection.strategy !== 'ad') {
                for (let domain of MOZILLA_STAFF_DOMAINS) {
                  // we need to sanitize the email address to lowercase before matching so we can catch users with upper/mixed case email addresses
                  if (event.user.email.toLowerCase().endsWith(domain)) {
                    const msg = `Staff or LDAP user attempted to login with the wrong login method. We only allow ad (LDAP) for staff: ${event.user.email}`;
          -         looger.info(msg);
          +         console.log(msg);
                    postError("staffmustuseldap");
                    return;
                  }
                }
              }
            
              return;
            }
        EOT
        id         = "357a3548-42d0-4cd5-87b2-bb68a46ed933"
        name       = "ensureLdapUsersUseLdap"
        # (3 unchanged attributes hidden)

      - dependencies {
          - name    = "@google-cloud/logging-winston" -> null
          - version = "6.0.0" -> null
        }
      - dependencies {
          - name    = "winston" -> null
          - version = "3.17.0" -> null
        }

      - secrets {
          - name  = "google_service_account_creds" -> null
          - value = (sensitive value) -> null
        }

        # (5 unchanged blocks hidden)
    }

  # auth0_action.gheGroups will be updated in-place
  ~ resource "auth0_action" "gheGroups" {
      ~ code       = <<-EOT
            // Required Libraries
          - const { LoggingWinston } = require('@google-cloud/logging-winston');
          - const winston = require('winston');
          + const fetch = require('node-fetch');
            
            exports.onExecutePostLogin = async (event, api) => {
          +   console.log("Running action:", "gheGroups");
            
          -   const serviceAccountCreds = JSON.parse(event.secrets.google_service_account_creds);
          - 
          -   // Create a GCP LoggingWinston transport with credentials
          -   const loggingWinston = new LoggingWinston({
          -     projectId: 'iam-auth0',
          -     logName: `auth0-action-${event.tenant.id}`,
          -     credentials: serviceAccountCreds,
          -     labels: {
          -       tenant: event.tenant.id || null,
          -       client_id: event.client.client_id || null,
          -       client_name: event.client.name || null,
          -       connection_id: event.connection.id || null,
          -       connection_name: event.connection.name || null,
          -       connection_strategy: event.connection.strategy || null,
          -       session_id: event.session.id || null,
          -       user_id: event.user.user_id || null,
          -       user_email: event.user.email || null,
          -       service: 'auth0-actions',
          -     },
          -   });
          - 
          -   // Create the Winston logger
          -   const logger = winston.createLogger({
          -     level: 'info', // Set the minimum log level
          -     transports: [
          -       // Add Google Cloud Logging transport
          -       loggingWinston,
          - 
          -       // Also log to the console
          -       new winston.transports.Console({
          -         format: winston.format.combine(
          -           winston.format.colorize(),
          -           winston.format.simple()
          -         ),
          -       }),
          -     ],
          -   });
          - 
          -   logger.info("Running action: gheGroups");
          - 
              // Object of applications with a 1:1 mapping of clientID and related group
              const applicationGroupMapping = {
              // Dev applications
                '9MR2UMAftbs6758Rmbs8yZ9Dj5AjeT0P': 'mozilliansorg_ghe_ghe-auth-dev_users',
            
              // Prod applications
                'RzaIwPS6wfABGLrhnCmWdzlCLoKXUY84': 'mozilliansorg_ghe_mozilla-actions_users',
                'EnEylt4OZW6i7yCWzZmCxyCxDRp6lOY0': 'mozilliansorg_ghe_saml-test-integrations_users',
                '2MVzcGFtl2rbdEx97rpC98urD6ZMqUcf': 'mozilliansorg_ghe_mozilla-it_users',
                'Cc2xFG6xS5O8UKoSzoJ4eNggo6jHnzDU': 'mozilliansorg_ghe_mozilla-games_users',
                '8lXCX2EGQNLixvBqONK3ceCVY2ppYiU6': 'mozilliansorg_ghe_mozilla-jetpack_users',
                'kkVlkyyJjjhONdxjiB4963i4cka6VBSh': 'mozilliansorg_ghe_mozilla-twqa_users',
                'nzV38DSEECYNl9toBfbVvVqXG04d2DaR': 'mozilliansorg_ghe_fxos_users',
                'RXTiiTCJ8wCCHklJuU9NxB1gK3GpFL4J': 'mozilliansorg_ghe_fxos-eng_users',
                '2mw77kvVsYBwlvZFay45S0e7dJ9Cd6z5': 'mozilliansorg_ghe_mozilla-b2g_users',
                'XBQy3ijpDhqnE9PQLd9fvO85o8NNroFH': 'mozilliansorg_ghe_mozilla-tw_users',
                'pFmfC1JoiDB9DZcrqX5GpiUM0IpDwIi5': 'mozilliansorg_ghe_mozillawiki_users',
                'F7KEqlRIgdC5yUAAq0zm4voJZFk9IlS4': 'mozilliansorg_ghe_mozilla-outreachy-datascience_users',
                'lhAIAsdx3jSOiKe1LoHmB0zEsUrCbfhI': 'mozilliansorg_ghe_moco-ghe-admin_users',
                'f1MpcTzYA8J06nUUdO5LuhhA7b4JZVJi': 'mozilliansorg_ghe_mozilla_users',
                's0v1r2d34lTqPtQu0jBVOKbWOKK4i1TU': 'mozilliansorg_ghe_mozmeao_users',
                '5GfQ2AMXMqibOsatSYTKh3dVSioVPhGA': 'mozilliansorg_ghe_mozrelops_users',
                'k2dBGcFJAhzlqOuSZH5nQhyq6L87jVaT': 'mozilliansorg_ghe_mozilla-svcops_users',
                'oU3JDtWZSeeBuUcJ0dfLKXU1S2tnTg0K': 'mozilliansorg_ghe_mozilla-applied-ml_users',
                'NyrIlf4H3ZYtMUfJLs6UmUwllOpfo23v': 'mozilliansorg_ghe_mozilla-iam_users',
                'TeSutPsFGcieGEIl30pL35lrZ4HDEim0': 'mozilliansorg_ghe_devtools-html_users',
                'HPl9z5rJS6mjRUNqkcr2avRZvnnXW1nI': 'mozilliansorg_ghe_mozilla-archive_users',
                '3agx8byruE6opXpzoAaJl1rvlS6JA8Ly': 'mozilliansorg_ghe_mozilla-commons_users',
                'Vyg4xo7d0ECLHaLD1DnLl1MYmziqv1SP': 'mozilliansorg_ghe_mozilla-lockbox_users',
                'npLk8377ceFcsXp5SIEJYwBqoXUn1zeu': 'mozilliansorg_ghe_mozilla-private_users',
                'qBv5vlRW7fNiIRIiuSjjZtoulwlUwo6L': 'mozilliansorg_ghe_mozilladpx_users',
                '4Op3cF3IvEHBGpD6gIFHHUlAXFGLiZWq': 'mozilliansorg_ghe_mozilla-frontend-infra_users',
                'IYfS3mWjTOnCX5YJ6mMWlBWEJyAwUAZm': 'mozilliansorg_ghe_mozilla-bteam_users',
                'tflU5Bd4CAzzlJzgDPT25Ks2CNADkuhZ': 'mozilliansorg_ghe_mozilla-conduit_users',
                'HHb263N55HitFj5bBVFanv2AnF6E6bGf': 'mozilliansorg_ghe_mozilla-sre-deploy_users',
                'bPCduBPyVFSxPEEdpG3dMdoiHXuj26Kr': 'mozilliansorg_ghe_firefox-devtools_users',
                'fqzPu0Hg17Vgx90JcWh1nWcV8TN4WkXa': 'mozilliansorg_ghe_iodide-project_users',
                'AcnyB9st2RTC6JfqizCSdaMlzBC7notV': 'mozilliansorg_ghe_mozilla-l10n_users',
                'fdGht0OM5DNTYPTWENtEhrXdGP6zmH9L': 'mozilliansorg_ghe_mozilla-lockwise_users',
                'aKU0bzGLTVv53jDokaUDwNUyNfZxgT4R': 'mozilliansorg_ghe_mozilla-spidermonkey_users',
                'Oy6exOuOGejAqExc8fZnSGdJA9t4njnG': 'mozilliansorg_ghe_mozillareality_users',
                '3iAAhN0vAavOHIzCqnaFKo9Mlqb9pBLH': 'mozilliansorg_ghe_mozillasecurity_users',
                'Qb2ZWerstBXCn5yCXQYU7vUfLuaZ1dMB': 'mozilliansorg_ghe_nss-dev_users',
                'A5hvTaSHqMyrCVMypE3TNhW4VXQzM63d': 'mozilliansorg_ghe_nubisproject_users',
                'VStrUcaxLXH9xQEEFX9Vkf0D5pRo5c6C': 'mozilliansorg_ghe_projectfluent_users',
                'WKOfTFaGTV10YKzfkMOyAl3bgi3BPFMc': 'mozilliansorg_ghe_taskcluster_users',
                '8Zhm4W07m9OSBlwN2h9FtQorFs6WgbQ8': 'mozilliansorg_ghe_mozilla-mobile_users',
                'vJG7CGVQutdCWpMGO9pkC5Vn4vgJzJ3I': 'mozilliansorg_ghe_mozilla-ocho_users',
                'dlDfXM5oqapRXUvrkCarPwgTN2INIA9G': 'mozilliansorg_ghe_mozilla-metrics_users',
                'lJbj6OE9VFK05i2XjZEiAEljamPyOCkz': 'mozilliansorg_ghe_mozilla-platform-ops_users',
                'AgiLB9xCoW4beavY9z7UuvO36DLmdwJ1': 'mozilliansorg_ghe_mozilla-rally_users',
                'QfJVAjXlaGzpCo5S48J9D38QvIfhlYzF': 'mozilliansorg_ghe_mozilladatascience_users',
                'UwUgLsXH6YtrWLATQpTuil2iNilYGGhF': 'mozilliansorg_ghe_mozilla-services_users',
                'RLPUxhCQsmmRHyOmDOGkLpu1mArNH3xn': 'mozilliansorg_ghe_firefoxux_users',
                'KMcYzqySOFXHteY1zliDlq577ARCb6gi': 'mozilliansorg_ghe_mozillasocial_users',
                'IEc83wZvZzcQXMkpUmrnb9P8wztUiokl': 'mozilliansorg_ghe_mozscout_users',
                'vkoDkHlCEUhlHNhVDtewJqRLVLGVsPrZ': 'mozilliansorg_ghe_mozilla-fakespot_users',
                'T6mjvGguOB5hkq9Aviaa58tOlwpJG5o6': 'mozilliansorg_ghe_mozilla-necko_users',
                'ZemrAl9S2q9GKJNQUdjZCNsLiVmSEg1P': 'mozilliansorg_ghe_mozilla-privacy_users',
                'sZHTTA4iuHgmiQGzbkS7lcXE1bbMGces': 'mozilliansorg_ghe_firefoxgraphics_users'
              };
            
              // ClientID isn't mapped here, return callback() and proceed rules processing
              if (applicationGroupMapping[event.client.client_id] === undefined) {
          -     logger.info("Not mapped");
          +     console.log("Not mapped");
                return;
              }
            
              // Consts
              const AUTH0_TIMEOUT = 5000;  // milliseconds
              const PERSONAPI_BEARER_TOKEN_REFRESH_AGE = 64770;  // 18 hours - 30 seconds for refresh timeout allowance
              const PERSONAPI_TIMEOUT = 5000;  // milliseconds
              const USER_ID = event.user.user_id;
            
              // Get a bearer token in order to access the person api
              const getBearerToken = async () => {
            
                const cachedBearerToken = api.cache.get("personapi_bearer_token");
                const cachedBearerTokenCreationTime = api.cache.get("personapi_bearer_token_creation_time");
                // If we have the bearer token stored, we don't need to fetch it again
                if ( cachedBearerToken?.value && cachedBearerTokenCreationTime?.value ) {
                  if (Date.now() - Number(cachedBearerTokenCreationTime.value) < PERSONAPI_BEARER_TOKEN_REFRESH_AGE) {
          +         console.log("Returning cached token", cachedBearerToken);
                    return cachedBearerToken.value;
                  }
                }
            
                const options = {
                  method: 'POST',
                  headers: {
                    'Content-Type': 'application/json',
                  },
                  timeout: AUTH0_TIMEOUT,
                  body: JSON.stringify({
                    audience: event.secrets.personapi_audience,
                    client_id: event.secrets.personapi_read_profile_api_client_id,
                    client_secret: event.secrets.personapi_read_profile_api_secret,
                    grant_type: 'client_credentials',
                  })
                };
            
                try {
                  const response = await fetch(event.secrets.personapi_oauth_url, options);
                  if (!response.ok) {
                    // Throw an error if the response status code is not in the 200-299 range
                    throw new Error(`HTTP error! status: ${response.status}`);
                  }
                  const data = await response.json();
            
                  // store the bearer token in the global object, so it's not constantly retrieved
                  api.cache.set("personapi_bearer_token", data.access_token);
                  api.cache.set("personapi_bearer_token_creation_time", String(Date.now()));
            
          -       logger.info(`Successfully retrieved bearer token from Auth0`);
          +       console.log(`Successfully retrieved bearer token from Auth0`);
                  return data.access_token;
                } catch (err) {
                  throw Error(`Unable to retrieve bearer token from Auth0: ${err}`);
                }
              };
            
            
              const getPersonProfile = async (bearerToken) => {
                try {
                  // Retrieve a bearer token to gain access to the person api
                  // return the profile data
                  const options = {
                    method: 'GET',
                    headers: {
                      'Authorization': `Bearer ${bearerToken}`,
                    },
                    timeout: PERSONAPI_TIMEOUT,
                  };
            
                  const url = `${event.secrets.personapi_url}/v2/user/user_id/${encodeURI(USER_ID)}`;
          -       logger.info(`Fetching person profile of ${USER_ID}`);
          +       console.log(`Fetching person profile of ${USER_ID}`);
                  const response = await fetch(url, options);
                    if (!response.ok) {
                      // Throw an error if the response status code is not in the 200-299 range
                      throw new Error(`HTTP error! status: ${response.status}`);
                    }
                  return await response.json();
                } catch (error) {
                  throw Error(`Unable to retrieve profile from Person API: ${error}`);
                }
              };
            
              const processProfile = (profile) => {
            
                // Create a new URL object
                const gheWikiUrl = new URL("https://wiki.mozilla.org/GitHub/SAML_issues");
            
                // Set the tenant searchParam
                gheWikiUrl.searchParams.set("auth", event.tenant.id);
            
                let errorCode;
            
                // Confirm the user has the group defined from mozillians matching the application id
                if(!event.user.app_metadata.groups?.includes(applicationGroupMapping[event.client.client_id])) {
                  errorCode = "ghgr";
                } else {
                  // Get githubUsername from person api, otherwise we'll redirect
                  let githubUsername;
            
                  try {
                    githubUsername = profile.usernames.values['HACK#GITHUB'];
                    // If profile does not hold key/value for githubUsername
                    if(githubUsername === undefined) {
          -           logger.info("githubUsername is undefined");
          +           console.log("githubUsername is undefined");
                      errorCode = "ghnd";
                    } else if (githubUsername.length === 0) {
                      // If somehow dinopark allows a user to store an empty value
          -           logger.info("empty HACK#GITHUB");
          +           console.log("empty HACK#GITHUB");
                      errorCode = "ghnd";
                    }
                  } catch (err) {
          -         logger.error("Unable to do the githubUsername lookup: " + err);
          +         console.log("Unable to do the githubUsername lookup: " + err);
                    errorCode = "ghul";
                  }
                }
            
                // confirm the user has a githubUsername stored in mozillians, otherwise redirect
                if (errorCode) {
                  // Set the search parameter error code
                  gheWikiUrl.searchParams.set("dbg", errorCode);
                  // Redirect the user
                  api.redirect.sendUserTo(gheWikiUrl.href);
                  return api.access.deny(`Access denied: See ${gheWikiUrl.href}`);
                }
                return;
              };
            
              // Main
              try {
                // Retrieve user's profiles
                const bearerToken = await getBearerToken();
                const userProfile = await getPersonProfile(bearerToken);
                return processProfile(userProfile);
              } catch (err) {
          -     logger.error(err);
          +     console.error(err);
                return api.access.deny(err);
              }
            }
        EOT
        id         = "137e2856-99ec-4097-8675-9a1a9287a939"
        name       = "gheGroups"
        # (3 unchanged attributes hidden)

      - dependencies {
          - name    = "@google-cloud/logging-winston" -> null
          - version = "6.0.0" -> null
        }
      - dependencies {
          - name    = "winston" -> null
          - version = "3.17.0" -> null
        }
      + dependencies {
          + name    = "node-fetch"
          + version = "2.7.0"
        }

      - secrets {
          - name  = "google_service_account_creds" -> null
          - value = (sensitive value) -> null
        }

        # (6 unchanged blocks hidden)
    }

  # auth0_action.linkUserByEmail will be updated in-place
  ~ resource "auth0_action" "linkUserByEmail" {
      ~ code       = <<-EOT
            /**
             * @title Link Accounts with Same Email Address while Merging Metadata
             * @overview Link any accounts that have the same email address while merging metadata.
             * @gallery true
             * @category access control
             *
             * This rule will link any accounts that have the same email address while merging metadata.
             * Source/Original: https://github.com/auth0/rules/blob/master/src/rules/link-users-by-email-with-metadata.js
             *
             * Please see https://github.com/mozilla-iam/mozilla-iam/blob/master/docs/deratcheting-user-flows.md#user-logs-in-with-the-mozilla-iam-system-for-the-first-time
             * for detailed explanation of what happens here.
             *
             */
            
          - const { LoggingWinston } = require('@google-cloud/logging-winston');
          - const winston = require('winston');
            const auth0Sdk = require("auth0");
            
            exports.onExecutePostLogin = async (event, api) => {
          +   console.log("Running actions:", "linkUsersByEmail");
            
          -   const serviceAccountCreds = JSON.parse(event.secrets.google_service_account_creds);
          - 
          -   // Create a GCP LoggingWinston transport with credentials
          -   const loggingWinston = new LoggingWinston({
          -     projectId: 'iam-auth0',
          -     logName: `auth0-action-${event.tenant.id}`,
          -     credentials: serviceAccountCreds,
          -     labels: {
          -       tenant: event.tenant.id || null,
          -       client_id: event.client.client_id || null,
          -       client_name: event.client.name || null,
          -       connection_id: event.connection.id || null,
          -       connection_name: event.connection.name || null,
          -       connection_strategy: event.connection.strategy || null,
          -       session_id: event.session.id || null,
          -       user_id: event.user.user_id || null,
          -       user_email: event.user.email || null,
          -       service: 'auth0-actions',
          -     },
          -   });
          - 
          -   // Create the Winston logger
          -   const logger = winston.createLogger({
          -     level: 'info', // Set the minimum log level
          -     transports: [
          -       // Add Google Cloud Logging transport
          -       loggingWinston,
          - 
          -       // Also log to the console
          -       new winston.transports.Console({
          -         format: winston.format.combine(
          -           winston.format.colorize(),
          -           winston.format.simple()
          -         ),
          -       }),
          -     ],
          -   });
          - 
          -   logger.info("Running actions: linkUsersByEmail");
          - 
              // Check if email is verified, we shouldn't automatically
              // merge accounts if this is not the case.
              if (!event.user.email || !event.user.email_verified) {
                return;
              }
            
              const mgmtAuth0Domain = event.tenant.id === "dev" ? "dev.mozilla-dev.auth0.com" : "auth.mozilla.auth0.com";
            
              // Create an Auth0 Management API Client
              const ManagementClient = auth0Sdk.ManagementClient;
              const mgmtClient = new ManagementClient({
                domain: mgmtAuth0Domain,
                clientId: event.secrets.mgmtClientId,
                clientSecret: event.secrets.mgmtClientSecret,
                scope: "update:users"
              });
            
              // Since email addresses within auth0 are allowed to be mixed case and the /user-by-email search endpoint
              // is case sensitive, we need to search for both situations.  In the first search we search by "this" users email
              // which might be mixed case (or not).  Our second search is for the lowercase equivalent but only if two searches
              // would be different.
              const searchMultipleEmailCases = async () => {
            
                let userAccountsFound = [];
            
                // Push the
                userAccountsFound.push(mgmtClient.usersByEmail.getByEmail({"email": event.user.email}));
            
                // if this user is mixed case, we need to also search for the lower case equivalent
                if (event.user.email !== event.user.email.toLowerCase()) {
                  userAccountsFound.push(mgmtClient.usersByEmail.getByEmail({"email": event.user.email.toLowerCase()}));
                }
            
                // await all json responses promises to resolve
                const allJSONResponses = await Promise.all(userAccountsFound);
            
                // flatten the array of arrays to get one array of profiles
                const mergedDataProfiles = allJSONResponses.reduce((acc, response) => {
                    return acc.concat(response.data);
                }, []);
            
                return mergedDataProfiles;
              };
            
              const linkAccount = async (otherProfile) => {
            
                // sanity check if both accounts have LDAP as primary
                // we should NOT link these accounts and simply allow the user to continue logging in.
                if (event.user.user_id.startsWith('ad|Mozilla-LDAP') && otherProfile.user_id.startsWith('ad|Mozilla-LDAP')) {
          -       logger.error(`Error: both ${event.user.user_id} and ${otherProfile.user_id} are LDAP Primary accounts. Linking will not occur.`);
          +       console.error(`Error: both ${event.user.user_id} and ${otherProfile.user_id} are LDAP Primary accounts. Linking will not occur.`);
                  return; // Continue with user login without account linking
                }
            
                // LDAP takes priority being the primary identity
                // So we need to determine if one or neither are LDAP
                // If both are non-primary, linking order doesn't matter
                let primaryUser;
                let secondaryUser;
            
                if (event.user.user_id.startsWith('ad|Mozilla-LDAP')) {
                  primaryUser = event.user;
                  secondaryUser = otherProfile;
                } else {
                  primaryUser = otherProfile;
                  secondaryUser = event.user;
                }
            
                // Link the secondary account into the primary account
          -     logger.info(
          +     console.log(
                  `Linking secondary identity ${secondaryUser.user_id} into primary identity ${primaryUser.user_id}`
                );
            
                // We no longer keep the user_metadata nor app_metadata from the secondary account
                // that is being linked.  If the primary account is LDAP, then its existing
                // metadata should prevail.  And in the case of both, primary and secondary being
                // non-ldap, account priority does not matter and neither does the metadata of
                // the secondary account.
            
                // Link the accounts
                try {
            
                  await mgmtClient.users.link(
                    { id: String(primaryUser.user_id) },
                    {
                      provider: secondaryUser.identities[0].provider,
                      user_id: secondaryUser.identities[0].user_id
                    }
                  );
            
                  // Auth0 Action api object provides a method for updating the current
                  // authenticated user to the new user_id after account linking has taken place
                  api.authentication.setPrimaryUser(primaryUser.user_id);
            
                } catch (err) {
          -       logger.error('An unknown error occurred while linking accounts: ' + err);
          +       console.log('An unknown error occurred while linking accounts: ' + err);
                  throw (err);
                }
            
                return;
              };
            
              // Main
              try {
                // Search for multiple accounts of the same user to link
                let userAccountList = await searchMultipleEmailCases();
            
                // Ignore non-verified users
                userAccountList = userAccountList.filter((u) => u.email_verified);
            
                if (userAccountList.length <= 1) {
                  // The user logged in with an identity which is the only one Auth0 knows about
                  // or no data returned
                  // Do not perform any account linking
                  return;
                }
            
                if (userAccountList.length === 2) {
                  // Auth0 is aware of 2 identities with the same email address which means
                  // that the user just logged in with a new identity that hasn't been linked
                  // into the other existing identity.  Here we pass the other account to the
                  // linking function
            
                  await linkAccount(userAccountList.filter((u) => u.user_id !== event.user.user_id)[0]);
            
                } else {
                  // data.length is > 2 which, post November 2020 when all identities were
                  // force linked manually, shouldn't be possible
                  var error_message =
                    `Error linking account ${event.user.user_id} as there are ` +
                    `over 2 identities with the email address ${event.user.email} ` +
                    userAccountList.map((x) => x.user_id).join();
          -       logger.error(error_message);
          +       console.error(error_message);
                  throw new Error(error_message);
                }
              } catch (err) {
          -     logger.error('An error occurred while linking accounts: ' + err);
          +     console.log('An error occurred while linking accounts: ' + err);
                return api.access.deny(err);;
              }
            
              return;
            }
        EOT
        id         = "441f971f-8ca7-451a-921d-1c031a3a2179"
        name       = "linkUserByEmail"
        # (3 unchanged attributes hidden)

      - dependencies {
          - name    = "@google-cloud/logging-winston" -> null
          - version = "6.0.0" -> null
        }
      - dependencies {
          - name    = "winston" -> null
          - version = "3.17.0" -> null
        }

      - secrets {
          - name  = "google_service_account_creds" -> null
          - value = (sensitive value) -> null
        }

        # (4 unchanged blocks hidden)
    }

  # auth0_action.samlMappings will be updated in-place
  ~ resource "auth0_action" "samlMappings" {
      ~ code       = <<-EOT
          - const { LoggingWinston } = require('@google-cloud/logging-winston');
          - const winston = require('winston');
          - 
            exports.onExecutePostLogin = async (event, api) => {
          +   console.log("Running actions:", "samlMappings");
            
          -   const serviceAccountCreds = JSON.parse(event.secrets.google_service_account_creds);
          - 
          -   // Create a GCP LoggingWinston transport with credentials
          -   const loggingWinston = new LoggingWinston({
          -     projectId: 'iam-auth0',
          -     logName: `auth0-action-${event.tenant.id}`,
          -     credentials: serviceAccountCreds,
          -     labels: {
          -       tenant: event.tenant.id || null,
          -       client_id: event.client.client_id || null,
          -       client_name: event.client.name || null,
          -       connection_id: event.connection.id || null,
          -       connection_name: event.connection.name || null,
          -       connection_strategy: event.connection.strategy || null,
          -       session_id: event.session.id || null,
          -       user_id: event.user.user_id || null,
          -       user_email: event.user.email || null,
          -       service: 'auth0-actions',
          -     },
          -   });
          - 
          -   // Create the Winston logger
          -   const logger = winston.createLogger({
          -     level: 'info', // Set the minimum log level
          -     transports: [
          -       // Add Google Cloud Logging transport
          -       loggingWinston,
          - 
          -       // Also log to the console
          -       new winston.transports.Console({
          -         format: winston.format.combine(
          -           winston.format.colorize(),
          -           winston.format.simple()
          -         ),
          -       }),
          -     ],
          -   });
          - 
          -   logger.info("Running actions: samlMappings");
          - 
              switch(event.client.client_id) {
            
                case "pFf6sBIfp4n3Wcs3F9Q7a9ry8MTrbi2F": // matrix-oidc
                  const preferred_username = event.user.email.split('@')[0];
                  api.idToken.setCustomClaim("preferred_username", preferred_username);
                  break;
            
                case "cPH0znP4n74JvPf9Efc1w6O8KQWwT634": // Tines
                  // Only pass relative groups. These should match the authorized apps in apps.yml
                  const tineGroups = [
                    'mozilliansorg_sec_tines-admin',
                    'mozilliansorg_sec_tines-access',
                    'team_moco',
                    'team_mofo',
                    'team_mzla',
                    'team_mzai',
                    'team_mzvc'
                    ];
                  const userGroups = event.user.app_metadata?.groups || [];
                  const selectGroups = tineGroups.filter(group => userGroups.includes(group));
                  api.samlResponse.setAttribute("http://sso.mozilla.com/claim/groups", selectGroups);
                  // DELETE the standard group claim
                  api.samlResponse.setAttribute("http://schemas.xmlsoap.org/claims/Group", null);
                  break;
            
                case "wgh8S9GaE7sJ4i0QrAzeMxFXgWZYtB0l": // sage-intacct
                  api.samlResponse.setAttribute('Company Name', 'MOZ Corp');
                  api.samlResponse.setAttribute('emailAddress', event.user.email);
                  api.samlResponse.setAttribute('name', event.user.name);
                  break;
            
                case "pUmRmcBrAJEdsgRTVXIW84jZoc3wtuYO": // planful-dev
                  api.idToken.setCustomClaim("IdP Entity ID", "urn:auth-dev.mozilla.auth0.com")
                  break;
            
                case "H5ddlJSCfGP8ab65EnWaB2sd541CJAlM": // planful
                  api.idToken.setCustomClaim("IdP Entity ID", "auth.mozilla.auth0.com")
                  break;
            
                // This can be move to the SAML settings of the application
                case "R4djNlyXSl3i8N2KXWkfylghDa9kFQ84": // thinksmart
                  api.samlResponse.setAttribute('Email', event.user.email);
                  api.samlResponse.setAttribute('firstName', event.user.given_name);
                  api.samlResponse.setAttribute('lastName', event.user.family_name);
                  break;
            
                // https://bugzilla.mozilla.org/show_bug.cgi?id=1637117
                case "cEfnJekrSStxxxBascTjNEDAZVUPAIU2": // stripe-subplat
                  const groupToStripeRoleMap = {
                    //  LDAP group name          stripe_role_name           stripe_account_id
                    'stripe_subplat_admin': [{'role': 'admin', 'account': 'acct_1EJOaaJNcmPzuWtR'}],
                    'stripe_subplat_developer': [{'role': 'developer', 'account': 'acct_1EJOaaJNcmPzuWtR'}],
                    'stripe_subplat_supportsp': [{'role': 'support_specialist', 'account': 'acct_1EJOaaJNcmPzuWtR'}],
                    'stripe_subplat_analyst': [{'role': 'analyst', 'account': 'acct_1EJOaaJNcmPzuWtR'}],
                    'stripe_subplat_viewonly': [{'role': 'view_only', 'account': 'acct_1EJOaaJNcmPzuWtR'}]
                  };
            
                  Object.keys(groupToStripeRoleMap).forEach((groupName) => {
                    if (event.user.hasOwnProperty('groups') && event.user.groups.includes(groupName)) {
                      groupToStripeRoleMap[groupName].forEach((roleInfo) => {
                        api.samlResponse.setAttribute(`Stripe-Role-${roleInfo.account}`, roleInfo.role);
                      });
                    }
                  });
                  break;
            
                case "inoLoMyAEOzLX1cZOvubQpcW18pk4O1S": // acoustic-stage
                case "sBImsybtPPLyWlstD0SC35IwnAafE4nB": // acoustic-prod
                  api.samlResponse.setAttribute('Nameid', event.user.email);
                  api.samlResponse.setAttribute('email', event.user.email);
                  api.samlResponse.setAttribute('firstName', event.user.given_name);
                  api.samlResponse.setAttribute('lastName', event.user.family_name);
                  break;
            
                case "eEAeYh6BMPfRyiSDax0tejjxkWi22zkP": // bitsight
                  api.samlResponse.setAttribute('urn:oid:0.9.2342.19200300.100.1.3', event.user.email);
                  api.samlResponse.setAttribute('urn:oid:2.5.4.3', event.user.given_name);
                  api.samlResponse.setAttribute('urn:oid:2.5.4.4', event.user.family_name);
                  // Assign BitSight roles based on group membership.
                  // https://help.bitsighttech.com/hc/en-us/articles/360008185714-User-Roles
                  // https://help.bitsighttech.com/hc/en-us/articles/231658167-SAML-Documentation
                  // Possible values :
                  //   Customer User
                  //   Customer Admin
                  //   Customer Group Admin
                  //   Customer Portfolio Manager
            
                  let bitsight_user_role;
                  if (event.user.groups?.includes('mozilliansorg_bitsight-admins')) {
                    bitsight_user_role = 'Customer Admin';
                  } else if (event.user.groups?.includes('mozilliansorg_bitsight-users')) {
                    bitsight_user_role = 'Customer Portfolio Manager';
                  } else {
                    bitsight_user_role = 'Customer User';
                  }
            
                  api.samlResponse.setAttribute('urn:oid:1.3.6.1.4.1.50993.1.1.2', bitsight_user_role);
                  break;
            
                case "q0tFB9QyFIKqPOOKvkFnHMj2VwrLjX46": // Google (test.mozilla.com)
                case "uYFDijsgXulJ040Os6VJLRxf0GG30OmC":
                  // This rule simply remaps @mozilla.com e-mail addresses to @test.mozilla.com to be used with the test.mozilla.com GSuite domain.
                  // Be careful when adding replacements not to do "double-replacements" where a replace replaces another rule. If that happens,
                  // you probably want to improve this code instead
                  let myemail;
                  if (event.client.client_id === "q0tFB9QyFIKqPOOKvkFnHMj2VwrLjX46") {
                    myemail = event.user.email.replace("mozilla.com", "test.mozilla.com").replace("mozillafoundation.org", "test.mozillafoundation.org").replace("getpocket.com", "test-gsuite.getpocket.com");
                  } else {
                    myemail = event.user.email.replace("mozilla.com", "gcp.infra.mozilla.com").replace("mozillafoundation.org", "gcp.infra.mozilla.com").replace("getpocket.com", "gcp.infra.mozilla.com");
                  }
            
                  api.samlResponse.setAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", myemail);
                  api.samlResponse.setAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", myemail);
                  api.samlResponse.setAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email", myemail);
                  api.samlResponse.setNameIdentifierFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:email")
                  break;
            
                case "RmsIEl3T3cZzpKhEmZv1XZDns0OvTzIy":
                  // Vectra expects one and only one group which happens to map to a single role on the Vectra side
                  // https://support.vectra.ai/s/article/KB-VS-1577
            
                  api.samlResponse.setAttribute("https://schema.vectra.ai/role", "mozilliansorg_sec_network_detection");
                  api.samlResponse.setAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", event.user.email);
                  api.samlResponse.setAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", event.user.name);
                  api.samlResponse.setAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", event.user.given_name);
                  api.samlResponse.setAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", event.user.family_name);
                  // TODO: this probably doesn't set the attribute to the upn claim.
                  api.samlResponse.setAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "upn");
                  break;
            
                case "gL08r5BRiweqf4aDQVX6xB4FHyFepFlM": // Navex - Stage
                case "iz2qSHo0lSv2nRZ8V3JnOESX5UR4dcpX": // Navex
                  api.samlResponse.setAttribute("PARTITION", "MOZILLA");
                  break;
            
                case "Ury9HCvBS4B1SzAH8f3YASbbcGf5QlQf":
                  // This rule sets a specific public key to encrypt the SAML assertion generated from Auth0
                  // and overrides the Issuer, because the client hardcodes a validation check for URL format
                  // TODO: In actions, the issuer cannot be overridden
            
                  //context.samlConfiguration.issuer = "https://auth.mozilla.auth0.com";
                  api.samlResponse.setEncryptionPublicKey("-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAo0pAWRHxJ3NWnItdWa7G\nsmBt4sQF7TlBDGDNUB55ojtl29ifLMfijmElgiBDwDn0IuzI+hKMHSCHlmBFvLMq\nIqJ36J//PPx6wVnkzuiRjKirRKP5CCbchF/McHH2cMi8SVrX2a+zIefPkLVoxDub\nAITQpmos/g5AkD07U/Js+130gTY1QJdYeJOOxkuJ9Afsrd0rJWvULh6+I/saP7zu\nSNMpPqYOxACXkqqdUMkTUE4EMhVIqcuw1qUO09JRjrGOkS1NKE+x7u8vpbevst9q\nntPglJ0730xx5cVJKXwQDWMXsxC4RSlrI6FZyryez0bwq5UGO9oBvtFsVy+rIWj2\nVSdzw7tmkrhED4oCItapgFLsKQWrKiRsCaWZOnW2Fz+cWFkepgelHE/oOZGBv+k3\nIvNZr7MxYLPPJQ7p4SMmT+TLPWXWmRGpL9uqE8ZwvGrUF4R1GzEQrVFd2NxbKzuO\nPHYwiPzzJNJwME541jL5A1cqsayEAXy0YltGGnofNa1mfk2PmfqfzZPXp79QOwW/\nNXPKNKAPgFI5g7zHQvbmnlnrOzUn8jrOHhxfZmY+hkQ0Mtju7H4L5AKJ5Dn7p2nv\nkK4HIymsXOdcj6WUcTi88yZX2yTXDnYtglXUIBKJVks6WiuF/yrhiaT2HLWa8WF0\nkD+1uOvqgm9nCKm7H6zHk7MCAwEAAQ==\n-----END PUBLIC KEY-----\n");
                  api.samlResponse.setEncryptionCert("-----BEGIN CERTIFICATE-----\nMIIFqDCCA5CgAwIBAgIELygDFTANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC\nR0IxFDASBgNVBAgTC094Zm9yZHNoaXJlMQ8wDQYDVQQHEwZPeGZvcmQxEzARBgNV\nBAoTClNlbW1sZSBMdGQxDTALBgNVBAsTBExHVE0xKjAoBgNVBAMTIUxHVE0gQXV0\nby1HZW5lcmF0ZWQgT25lbG9naW4gY2VydDAeFw0xOTA1MjEwNjEyNTdaFw0yMjA1\nMjAwNjEyNTdaMIGEMQswCQYDVQQGEwJHQjEUMBIGA1UECBMLT3hmb3Jkc2hpcmUx\nDzANBgNVBAcTBk94Zm9yZDETMBEGA1UEChMKU2VtbWxlIEx0ZDENMAsGA1UECxME\nTEdUTTEqMCgGA1UEAxMhTEdUTSBBdXRvLUdlbmVyYXRlZCBPbmVsb2dpbiBjZXJ0\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAo0pAWRHxJ3NWnItdWa7G\nsmBt4sQF7TlBDGDNUB55ojtl29ifLMfijmElgiBDwDn0IuzI+hKMHSCHlmBFvLMq\nIqJ36J//PPx6wVnkzuiRjKirRKP5CCbchF/McHH2cMi8SVrX2a+zIefPkLVoxDub\nAITQpmos/g5AkD07U/Js+130gTY1QJdYeJOOxkuJ9Afsrd0rJWvULh6+I/saP7zu\nSNMpPqYOxACXkqqdUMkTUE4EMhVIqcuw1qUO09JRjrGOkS1NKE+x7u8vpbevst9q\nntPglJ0730xx5cVJKXwQDWMXsxC4RSlrI6FZyryez0bwq5UGO9oBvtFsVy+rIWj2\nVSdzw7tmkrhED4oCItapgFLsKQWrKiRsCaWZOnW2Fz+cWFkepgelHE/oOZGBv+k3\nIvNZr7MxYLPPJQ7p4SMmT+TLPWXWmRGpL9uqE8ZwvGrUF4R1GzEQrVFd2NxbKzuO\nPHYwiPzzJNJwME541jL5A1cqsayEAXy0YltGGnofNa1mfk2PmfqfzZPXp79QOwW/\nNXPKNKAPgFI5g7zHQvbmnlnrOzUn8jrOHhxfZmY+hkQ0Mtju7H4L5AKJ5Dn7p2nv\nkK4HIymsXOdcj6WUcTi88yZX2yTXDnYtglXUIBKJVks6WiuF/yrhiaT2HLWa8WF0\nkD+1uOvqgm9nCKm7H6zHk7MCAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNVHQ8B\nAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAH/xAuVUXRDGo5vn/uERfssPc8Fa\nyL0wurpoy5jXVvYALSZouNGG26M6kJ+UTaxwBMm0zk3hGOE24qiIMNoDLupwsVFq\n8r9DsbD2hbcIqwzReI03KiKZ4PBBugV/I4nZVpu69yxk+lfNPW34CRYuRQGcISbA\nVIh5MS6fp2+7eCdxGCobLPMUmGSitgJUzUlvIIvvIyQ9mPP4S5MnIjNEnE7qolmz\nhPz2cLTJzRAVtOc2QAtMFEBysIXzJ5X3xkN750dflgHeo5voX07J/PEUN1vfTBBN\n8WJZBfqgNXauARnDCUsOrN+5NeBXmURiSrO+JGJu72Bwbabuw44EwrPap5otC/Hu\nTDIHJy/MnPmwXAhiW7jY9luNxtJL/9DfBEHNHU4AF3/0D90NU6artINqwKCebr/8\nlX4xmavcXRXh3EP6iqaCG+zpdyCquuE3GaCv48VY7WzKiajDE6abmy78nmu7nk++\n+7aGLMisf4CNIBDL9L6ZvdgHV2Oaom7h5P2L0Z0OfslE4C+IpAI+9lxcMzTOJHTf\n0khlXKceA5ky+1rne4IezyUbvwAKJ32M99yYRvCyevJW9XpoVQIYLc/iVbi5VjxL\nQGFqYSnLIlzudgiJq5x/24VqLB8EC5H+6XzLAzAolwYj/CKTBQsBIQqa/CKa6nOu\nyZliiPtDlnK3bBeY\n-----END CERTIFICATE-----\n");
                  break;
            
                case "x7TF6ZtJev4ktoHR4ObWmA9KeqGni6rq": // Braintree
                  api.samlResponse.setAttribute("grant_all_merchant_accounts", "true");
                  api.samlResponse.setAttribute("roles", event.user.app_metadata.groups);
                  break;
              }
            
              return;
            }
        EOT
        id         = "d7b26bc7-8265-459f-94d0-8db654c16b67"
        name       = "samlMappings"
        # (3 unchanged attributes hidden)

      - dependencies {
          - name    = "@google-cloud/logging-winston" -> null
          - version = "6.0.0" -> null
        }
      - dependencies {
          - name    = "winston" -> null
          - version = "3.17.0" -> null
        }

      - secrets {
          - name  = "google_service_account_creds" -> null
          - value = (sensitive value) -> null
        }

        # (1 unchanged block hidden)
    }

  # auth0_action.testing will be destroyed
  # (because auth0_action.testing is not in configuration)
  - resource "auth0_action" "testing" {
      - code       = <<-EOT
            const { LoggingWinston } = require('@google-cloud/logging-winston');
            const winston = require('winston');
            
            exports.onExecutePostLogin = async (event, api) => {
              const serviceAccountCreds = JSON.parse(event.secrets.google_service_account_creds);
            
              // Create a GCP LoggingWinston transport with credentials
              const loggingWinston = new LoggingWinston({
                projectId: 'iam-auth0',
                logName: `auth0-action-${event.tenant.id}`,
                credentials: serviceAccountCreds,
                labels: {
                  tenant: event.tenant.id || null,
                  client_id: event.client.client_id || null,
                  client_name: event.client.name || null,
                  connection_id: event.connection.id || null,
                  connection_name: event.connection.name || null,
                  connection_strategy: event.connection.strategy || null,
                  session_id: event.session.id || null,
                  user_id: event.user.user_id || null,
                  user_email: event.user.email || null,
                  service: 'auth0-actions',
                },
              });
            
              // Create the Winston logger
              const logger = winston.createLogger({
                level: 'info', // Set the minimum log level
                transports: [
                  // Add Google Cloud Logging transport
                  loggingWinston,
            
                  // Also log to the console
                  new winston.transports.Console({
                    format: winston.format.combine(
                      winston.format.colorize(),
                      winston.format.simple()
                    ),
                  }),
                ],
              });
            
              logger.info("Running action: Testing");
            
              let apps = ['2KNOUCxN8AFnGGjDCGtqiDIzq8MKXi2h'];
            
              if (apps.indexOf(event.client.client_id) >= 0) {
            
                // Log messages
                logger.info('This log entry will appear in Google Cloud Logging');
                logger.error('This is an error log');
                return;
              }
              return;
            }
        EOT -> null
      - deploy     = true -> null
      - id         = "837f9e60-1fce-42d9-bf33-008c75cc8cb2" -> null
      - name       = "testing" -> null
      - runtime    = "node18" -> null
      - version_id = "667c7a0a-55f4-4f1e-a59b-4e1c589dfa87" -> null

      - dependencies {
          - name    = "@google-cloud/logging-winston" -> null
          - version = "6.0.0" -> null
        }
      - dependencies {
          - name    = "winston" -> null
          - version = "3.17.0" -> null
        }

      - secrets {
          - name  = "google_service_account_creds" -> null
          - value = (sensitive value) -> null
        }

      - supported_triggers {
          - id      = "post-login" -> null
          - version = "v3" -> null
        }
    }

  # auth0_trigger_actions.login_flow will be updated in-place
  ~ resource "auth0_trigger_actions" "login_flow" {
        id      = "post-login"
        # (1 unchanged attribute hidden)

      - actions {
          - display_name = "testing" -> null
          - id           = "837f9e60-1fce-42d9-bf33-008c75cc8cb2" -> null
        }

        # (9 unchanged blocks hidden)
    }

  # google_project_iam_binding.log_writer will be destroyed
  # (because google_project_iam_binding.log_writer is not in configuration)
  - resource "google_project_iam_binding" "log_writer" {
      - etag    = "BwYr7U8ut5Y=" -> null
      - id      = "iam-auth0/roles/logging.logWriter" -> null
      - members = [
          - "serviceAccount:[email protected]",
        ] -> null
      - project = "iam-auth0" -> null
      - role    = "roles/logging.logWriter" -> null
    }

  # google_service_account.auth0_actions_logger will be destroyed
  # (because google_service_account.auth0_actions_logger is not in configuration)
  - resource "google_service_account" "auth0_actions_logger" {
      - account_id   = "auth0-actions-logger-dev" -> null
      - disabled     = false -> null
      - display_name = "Auth0 Actions Logger Service Account dev" -> null
      - email        = "[email protected]" -> null
      - id           = "projects/iam-auth0/serviceAccounts/[email protected]" -> null
      - member       = "serviceAccount:[email protected]" -> null
      - name         = "projects/iam-auth0/serviceAccounts/[email protected]" -> null
      - project      = "iam-auth0" -> null
      - unique_id    = "101377739404855947227" -> null
        # (1 unchanged attribute hidden)
    }

Plan: 0 to add, 10 to change, 3 to destroy.

Copy link

@gcoxmoz gcoxmoz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple group-add, does what the ticket says.

@bheesham bheesham merged commit bf8c2b9 into mozilla-iam:master Jan 20, 2025
1 check passed
@bheesham bheesham deleted the taskcluster branch January 20, 2025 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants