How to relate userId in perUserKeyManagerFactory with virtual users from different scenarios

Hi everybody,

We have developed a Gatling loadtest testing an Application Lifecycle Management (ALM) tool. There are four scenarios:

  • requirements-engineer
  • lead-engineer
  • test-engineer
    *business admin

for each of these scenarios I want to have a httpProtocol with perUserKeyManager property so that the virtual users can perform a X509 authentication against keycloak using the .p12 certificate that are named after them.

Thus in advance of the load test we prepare certificate per user, e.g.

requirements-engineer-1.p12 … requirements-engineer-n.p12 .
lead-engineer-1.p12 … lead-engineer-n.p12
and so on.

It could be said that there is a certificate pool per user role prepared in advance of the test.

We tried to configure a httpProtocol per scenario hoping that userIds would somehow be naturally divided according to the separate protocols.

  val httpProtocolReqEng = http.baseUrl("http://localhost:8080")
    .inferHtmlResources(AllowList(), DenyList(""".*\.js""", """.*\.css""", """.*\.gif""", """.*\.jpeg""", """.*\.jpg""", """.*\.ico""", """.*\.woff""", """.*\.woff2""", """.*\.(t|o)tf""", """.*\.png""", """.*\.svg""", """.*detectportal\.firefox\.com.*"""))
    .userAgentHeader("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")
    .disableWarmUp
    .perUserKeyManagerFactory{userId =>
      val keyStore = KeyStore.getInstance("PKCS12")
      val keyStorePassword = "password"
      val keyStoreFile = new File(s"path\\requirements-engineer-$userId.p12")
      keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword.toCharArray)
      val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
      keyManagerFactory.init(keyStore, keyStorePassword.toCharArray)

      keyManagerFactory}

  val httpProtocolLeadEng = http.baseUrl("http://localhost:8080")
    .inferHtmlResources(AllowList(), DenyList(""".*\.js""", """.*\.css""", """.*\.gif""", """.*\.jpeg""", """.*\.jpg""", """.*\.ico""", """.*\.woff""", """.*\.woff2""", """.*\.(t|o)tf""", """.*\.png""", """.*\.svg""", """.*detectportal\.firefox\.com.*"""))
    .userAgentHeader("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")
    .disableWarmUp
    .perUserKeyManagerFactory{userId =>
      val keyStore = KeyStore.getInstance("PKCS12")
      val keyStorePassword = "password"
      val keyStoreFile = new File(s"path\\lead-engineer-$userId.p12")
      keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword.toCharArray)
      val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
      keyManagerFactory.init(keyStore, keyStorePassword.toCharArray)

      keyManagerFactory}

However we noticed that the Gatling userId is incremented irrespective of the httpProtocol, depending (I presume through logs and docs), on the total number of users given the setup clause below.

I need to match the virtual user instance wtih a specific certificate by means of the userId. So in a way

if I know that the userId 2 is a lead engineer, I’d like keyManager to go pick up a certificate from the lead engineer certificate pool,

if I then know userId 4 is a lead engineer I’d like keyManager to pick up the next available certificate from the pool

It would be okay to have round-robin here so that once all avaialble certificates are used, it can start picking certificates from the beginning.

The experimental setup we made in our unsuccessful attempt is as below. we try to log each user before the load test because of the way keylcloak integrates with our app. Basically a user needs to login so that it appears as valid user over the application that we’re testing. So before preparing test data and running the test, we got to login all the users.

  private def inject(scenario: ScenarioBuilder, from: Int, to: Int): PopulationBuilder = {
    scenario.inject(
      rampConcurrentUsers(from).to(to).during(rampDuringSeconds),
      constantConcurrentUsers(to).during(keepForSeconds))
  }

  setUp(
    preparationReqEng.inject(
        atOnceUsers(number of REs)
      ).protocols(httpProtocolRequirementEngineer)
      .andThen(
        preparationLeadEng.inject(
            atOnceUsers(number of LEs)
          ).protocols(httpProtocolLeadEngineer)
          .andThen(
            preparationTestEng.inject(
                atOnceUsers(number of TEs)
              ).protocols(httpProtocolTestEngineer)
              .andThen(
                preparation.inject(
                    atOnceUsers(if (withPreparation) 1 else 0)
                  ).protocols(httpProtocolAdmin)
                  .andThen(
                    inject(requirementsEngineer, rampNumberOfRequirementsEngineersFrom, toNumberOfRequirementsEngineers).protocols(httpProtocolRequirementEngineer),
                    inject(businessAdministrator, rampNumberOfBusinessAdministratorsFrom, toNumberOfBusinessAdministrators).protocols(httpProtocolAdmin),
                    inject(leadEngineer, rampNumberOfLeadEngineersFrom, toNumberOfLeadEngineers).protocols(httpProtocolLeadEngineer),
                    inject(testEngineer, rampNumberOfTestEngineersFrom, toNumberOfTestEngineers).protocols(httpProtocolTestEngineer)
                  )
              )
          )
      )
  )

I’ve also read this piece on the community belog that is helpful but not quite specific to the problem we are facing because of the way we need to associate userIds with scenarios.

List item

If you want an incremental counter for each population, you can use an AtomicInteger for each that you increment in the function.

Thanks for your reply Stephane. Would it be possible for you provide some snippet or doc? It would help us a great deal since I at least am not a an experience developer.

ok so we’ve implemented atomic integers the following way, it appears to work so far as expected. Any comments / criticism / feedback is welcome.

  val requirementEngineerId = new AtomicInteger(0)
  val leadEngineerId = new AtomicInteger(0)
  val testEngineerId = new AtomicInteger(0)


  // http protocol settings
  private val httpProtocolRequirementEngineer: HttpProtocolBuilder = {
    http
      .baseUrl(url)
      .inferHtmlResources(AllowList(), DenyList(""".*\.js""", """.*\.css""", """.*\.gif""", """.*\.jpeg""", """.*\.jpg""", """.*\.ico""", """.*\.woff""", """.*\.woff2""", """.*\.(t|o)tf""", """.*\.png""", """.*\.svg""", """.*detectportal\.firefox\.com.*"""))
      .userAgentHeader("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")
      .perUserKeyManagerFactory { any =>
        val keyStore = KeyStore.getInstance("PKCS12")
        val keyStorePassword = "changeme"
        val keyStoreFile = new File(s"C:\\certs\\requirements-engineer-${requirementEngineerId.incrementAndGet()}.p12")
        keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword.toCharArray)
        val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
        keyManagerFactory.init(keyStore, keyStorePassword.toCharArray)

        keyManagerFactory
      }
  }


  private val httpProtocolLeadEngineer: HttpProtocolBuilder = {
    http
      .baseUrl(url)
      .inferHtmlResources(AllowList(), DenyList(""".*\.js""", """.*\.css""", """.*\.gif""", """.*\.jpeg""", """.*\.jpg""", """.*\.ico""", """.*\.woff""", """.*\.woff2""", """.*\.(t|o)tf""", """.*\.png""", """.*\.svg""", """.*detectportal\.firefox\.com.*"""))
      .userAgentHeader("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")
      .perUserKeyManagerFactory { any =>
        val keyStore = KeyStore.getInstance("PKCS12")
        val keyStorePassword = "changeme"
        val keyStoreFile = new File(s"C:\\certs\\lead-engineer-${leadEngineerId.incrementAndGet()}.p12")
        keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword.toCharArray)
        val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
        keyManagerFactory.init(keyStore, keyStorePassword.toCharArray)

        keyManagerFactory
      }
  }

  private val httpProtocolTestEngineer: HttpProtocolBuilder = {
    http
      .baseUrl(url)
      .inferHtmlResources(AllowList(), DenyList(""".*\.js""", """.*\.css""", """.*\.gif""", """.*\.jpeg""", """.*\.jpg""", """.*\.ico""", """.*\.woff""", """.*\.woff2""", """.*\.(t|o)tf""", """.*\.png""", """.*\.svg""", """.*detectportal\.firefox\.com.*"""))
      .userAgentHeader("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")
      .perUserKeyManagerFactory { any =>
        val keyStore = KeyStore.getInstance("PKCS12")
        val keyStorePassword = "changeme"
        val keyStoreFile = new File(s"C:\\certs\\test-engineer-${testEngineerId.incrementAndGet()}.p12")
        keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword.toCharArray)
        val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
        keyManagerFactory.init(keyStore, keyStorePassword.toCharArray)

        keyManagerFactory
      }
  }

Hi @einanc,

Except the code duplication (I think you can extract common code into a function):

  • hard coded values will be difficult to change when needed (password, common path)
  • Using file when the code only requires InputStream may be an issue at some point (when you want to have it in classpath resources in a jar)
  • By convention unused argument are underscore (_)
  • You should close the InputStream (or use Using construct)
  • You may avoid file manipulation during the load with preloading all certs
  private def preloadCerts(keyStorePassword: Array[Char], minInclusive: Int, maxInclusive: Int, getInputStream: Int => InputStream): Array[KeyManagerFactory] = (for {
    id <- minInclusive to maxInclusive
  } yield {
    val keyStore = KeyStore.getInstance("PKCS12")
    Using.resource(getInputStream(id)) { inputStream =>
      keyStore.load(inputStream, keyStorePassword)
      val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
      keyManagerFactory.init(keyStore, keyStorePassword)
      keyManagerFactory
    }
  }).toArray

  private def configuredProtocol(url: String, keyManagersFactories: Array[KeyManagerFactory]): HttpProtocolBuilder = {
    val incrementer = new AtomicInteger(0)

    http
      .baseUrl(url)
      .inferHtmlResources(AllowList(), DenyList(""".*\.js""", """.*\.css""", """.*\.gif""", """.*\.jpeg""", """.*\.jpg""", """.*\.ico""", """.*\.woff""", """.*\.woff2""", """.*\.(t|o)tf""", """.*\.png""", """.*\.svg""", """.*detectportal\.firefox\.com.*"""))
      .userAgentHeader("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")
      .perUserKeyManagerFactory { _ =>
        keyManagersFactories(incrementer.incrementAndGet() % keyManagersFactories.length)
      }
  }

  // http protocol settings
  private val httpProtocolRequirementEngineer: HttpProtocolBuilder =
    configuredProtocol(url, preloadCerts("changeme".toCharArray, 1, 10, id => Files.newInputStream(Path.of(s"C:\\certs\\requirements-engineer-$id.p12"))))

  private val httpProtocolLeadEngineer: HttpProtocolBuilder =
    configuredProtocol(url, preloadCerts("changeme".toCharArray, 1, 20, id => Files.newInputStream(Path.of(s"C:\\certs\\lead-engineer-$id.p12"))))

  private val httpProtocolTestEngineer: HttpProtocolBuilder =
    configuredProtocol(url, preloadCerts("changeme".toCharArray, 1, 30, id => Files.newInputStream(Path.of(s"C:\\certs\\test-engineer-$id.p12"))))

Note: I changed from File and FileInputStream to nio variant. Just because I personally prefer not to have “new” when possible.
I didn’t change the file context, because I don’t want to change your code behavior, but you should consider to make a variable (environment variable or java system properties) to be able to change the root dir (ie c:\certs)

This form allow to manage to transform the id before getting the actual file (a modulo for instance to stay betwen 0 and your max available cert, etc.)

I hope this helps!
Cheers!

(edit: Using.resource)
(edit2: preload files)

1 Like

Thank you for your reply @sbrevet it appears quite helpful.

Regarding this part:

Note: I changed from File and FileInputStream to nio variant. Just because I personally prefer not to have “new” when possible.
I didn’t change the file context, because I don’t want to change your code behavior, but you should consider to make a variable (environment variable or java system properties) to be able to change the root dir (ie c:\certs )

I can say that it is also quite to point. We will run the test on a cloud environment, and there we will need to manage this. In fact one of our request bodies is an Excel file and we had to do the following:

  private val excelFile = getClass.getClassLoader.getResourceAsStream("bodies/importExcel.xlsx").readAllBytes()
  private val excelFileSize = excelFile.length // This gives you the file size in bytes

otherwise there were exceptions like File System Not Found during the execution on the cloud env. I guess we will need a similar strategy with the certificates.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.