conjur-api 6.3.1.pre.846 → 6.3.1.pre.848

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a22e04544db452dd276f8053e60ec40ba19dbeb6c8c74fa554f1abad9b9ddb37
4
- data.tar.gz: 7997ace31344e73979de8a5e651526e0ca419862841e21475b5dec915ee5e444
3
+ metadata.gz: e94af8f767fd0324c87edb5648af755334c349670c1b4d50a2b4cec6927eb2f5
4
+ data.tar.gz: e5a01b8576510b3f21209b2a75790ee6f37607c011a8d0937481386e78d0746f
5
5
  SHA512:
6
- metadata.gz: 36941d33ca17af4c45f9f475ae0dc25161a573bdb3c6d4a5f4c4d457078a1bc122fd63872d9983b835467a78e17ee0a0e0e6b5fd8186a95fd680459bf1f59e31
7
- data.tar.gz: 0325c9377ed9aacd040f9d4cc0615c2ee0091460de22b52779cf4abec8fe9ed4e7d2ec6ec2ddde41fbecd9ad7099c2b1c3959776bcabd6275542afee8d592ba3
6
+ metadata.gz: 74c14463069c05daaa17c1c9aca5ffec2cc884835762c4a0bffaa0322158a3c09145badf7257b8bfdb19888b98b6298e548973111aa4463465da6f584621b95b
7
+ data.tar.gz: 6ed18e8e9acb13891834d729cd1fc5a4b678942b548be8418c58480517b90b0b28870a0a966dd176e2302e41e8026880d77acbfebd9eb027c0cfb7c9e0fb280d
data/CHANGELOG.md CHANGED
@@ -7,9 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
  ## Unreleased
8
8
  ### Added
9
9
  - Cloud-native authenticators for Azure Managed Identity (`authn-azure`),
10
- AWS IAM (`authn-iam`), and GCP workload identity (`authn-jwt/gcp`), exposed
10
+ AWS IAM (`authn-iam`), and GCP workload identity (`authn-gcp`), exposed
11
11
  via `Conjur::API.new_from_azure`, `.new_from_iam`, and `.new_from_gcp`
12
12
  (CNJR-14213).
13
+ - Live integration features for the cloud authenticators, gated behind the
14
+ `RUN_AWS_TESTS` / `RUN_AZURE_TESTS` / `RUN_GCP_TESTS` pipeline params and run
15
+ on dedicated cloud agents (excluded from the default suite) (CNJR-14213).
13
16
 
14
17
  ### Changed
15
18
  - CI uses Conjur Enterprise 1.27.0+, which removed the `GET /public_keys` endpoint
@@ -24,6 +27,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
24
27
  - Set `CONJUR_AUTHN_ALLOWED_IP_RANGES` and `CONJUR_AUTHN_HTTP_ALLOWLIST` in the
25
28
  test environment so Conjur's new SSRF guard does not block the authn-oidc
26
29
  `provider-uri` fetch to the compose-local Keycloak server. (CNJR-13786)
30
+ - Corrected `Conjur::API.new_from_gcp` / `authenticate_gcp` to target the
31
+ serviceless `authn-gcp` endpoint (`/authn-gcp/{account}/authenticate`) instead
32
+ of `authn-jwt`; the `service_id` argument was removed accordingly (CNJR-14213).
27
33
 
28
34
  ## [6.3.1] - 2026-07-16
29
35
 
data/Jenkinsfile CHANGED
@@ -30,8 +30,23 @@ pipeline {
30
30
  }
31
31
 
32
32
  triggers {
33
- cron(getDailyCronString())
34
- parameterizedCron(getWeeklyCronString("H(1-5)","%MODE=RELEASE"))
33
+ // Declarative pipelines allow only one parameterizedCron trigger, so both
34
+ // schedules live in a single multi-line string. The daily build also
35
+ // exercises the cloud authenticators end-to-end; PR builds leave the
36
+ // RUN_*_TESTS params false, so those stages are skipped there.
37
+ parameterizedCron("""
38
+ ${getDailyCronString("%RUN_AWS_TESTS=true;RUN_AZURE_TESTS=true;RUN_GCP_TESTS=true")}
39
+ ${getWeeklyCronString("H(1-5)","%MODE=RELEASE")}
40
+ """)
41
+ }
42
+
43
+ parameters {
44
+ booleanParam(name: 'RUN_AWS_TESTS', defaultValue: false,
45
+ description: 'Run the authn-iam integration test on an AWS executor agent')
46
+ booleanParam(name: 'RUN_AZURE_TESTS', defaultValue: false,
47
+ description: 'Run the authn-azure integration test on an Azure executor agent')
48
+ booleanParam(name: 'RUN_GCP_TESTS', defaultValue: false,
49
+ description: 'Run the authn-jwt/gcp integration test (token fetched on a GCP agent)')
35
50
  }
36
51
 
37
52
  stages {
@@ -61,6 +76,17 @@ pipeline {
61
76
  steps {
62
77
  script {
63
78
  infrapool = getInfraPoolAgent.connected(type: "ExecutorV2", quantity: 1, duration: 1)[0]
79
+ // Provision dedicated cloud agents only when the matching cloud test
80
+ // is requested. These are real Azure/GCP VMs whose managed identity /
81
+ // service account the authenticators exercise against the live
82
+ // metadata service. The AWS test reuses the standard ExecutorV2 agent
83
+ // (which itself runs in AWS with an instance profile).
84
+ if (params.RUN_AZURE_TESTS) {
85
+ azureInfrapool = getInfraPoolAgent.connected(type: "AzureExecutorV2", quantity: 1, duration: 1)[0]
86
+ }
87
+ if (params.RUN_GCP_TESTS) {
88
+ gcpInfrapool = getInfraPoolAgent.connected(type: "GcpExecutorV2", quantity: 1, duration: 1)[0]
89
+ }
64
90
  }
65
91
  }
66
92
  }
@@ -125,6 +151,12 @@ pipeline {
125
151
  script {
126
152
  infrapool.agentSh "./test.sh"
127
153
  infrapool.agentStash name: 'reports4.0', includes: '**/reports/*.xml'
154
+ // Stash coverage from the full suite now. The cloud stages below run
155
+ // ./test.sh again on this agent, and jenkins_init wipes and regenerates
156
+ // coverage/ from a single cloud feature (no rspec) — which would drop
157
+ // the reported coverage well below the quality gate. Capturing it here
158
+ // keeps the submitted coverage that of the complete unit+cucumber run.
159
+ infrapool.agentStash name: 'coverage', includes: '**/coverage/**'
128
160
  }
129
161
  }
130
162
  post {
@@ -134,16 +166,101 @@ pipeline {
134
166
  }
135
167
  }
136
168
 
137
- stage('Submit Coverage Report'){
138
- steps{
169
+ stage('Run AWS IAM tests') {
170
+ when { expression { params.RUN_AWS_TESTS } }
171
+ environment {
172
+ INFRAPOOL_RUN_AWS_TESTS = 'true'
173
+ INFRAPOOL_RUBY_VERSION = '3.3'
174
+ INFRAPOOL_REGISTRY_URL = "registry.tld"
175
+ }
176
+ steps {
139
177
  script {
140
- infrapool.agentStash name: 'coverage', includes: '**/coverage/**'
178
+ // authn-iam identifies the Conjur host by the AWS caller identity, so
179
+ // the host id in the policy must be "<account-id>/<role-name>". Derive
180
+ // those from the agent's own instance profile (as conjur-api-java does)
181
+ // and pass them to the feature via env.
182
+ def callerIdentityJson = infrapool.agentSh(script: 'aws sts get-caller-identity', returnStdout: true).trim()
183
+ def awsAccountId = (callerIdentityJson =~ /"Account"\s*:\s*"([^"]+)"/)[0][1]
184
+ def awsArn = (callerIdentityJson =~ /"Arn"\s*:\s*"([^"]+)"/)[0][1]
185
+ def awsRoleName = awsArn.tokenize('/')[1] // arn:...:assumed-role/<role>/<session>
186
+ infrapool.agentSh """
187
+ export INFRAPOOL_AWS_ACCOUNT_ID='${awsAccountId}'
188
+ export INFRAPOOL_AWS_ROLE_NAME='${awsRoleName}'
189
+ ./test.sh
190
+ """
191
+ }
192
+ }
193
+ post {
194
+ always {
195
+ script { infrapool.agentStash name: 'reports-aws', includes: '**/reports/*.xml' }
196
+ unstash 'reports-aws'
197
+ }
198
+ }
199
+ }
200
+
201
+ stage('Run Azure tests') {
202
+ when { expression { params.RUN_AZURE_TESTS } }
203
+ environment {
204
+ INFRAPOOL_RUN_AZURE_TESTS = 'true'
205
+ INFRAPOOL_RUBY_VERSION = '3.3'
206
+ INFRAPOOL_REGISTRY_URL = "registry.tld"
207
+ }
208
+ steps {
209
+ script {
210
+ // summon injects AZURE_SUBSCRIPTION_ID / AZURE_RESOURCE_GROUP /
211
+ // USER_ASSIGNED_IDENTITY_CLIENT_ID from the CI vault; the test hits
212
+ // the real Azure IMDS on this Azure VM.
213
+ azureInfrapool.agentSh "summon -e azure ./test.sh"
214
+ }
215
+ }
216
+ post {
217
+ always {
218
+ script { azureInfrapool.agentStash name: 'reports-azure', includes: '**/reports/*.xml' }
219
+ unstash 'reports-azure'
220
+ }
221
+ }
222
+ }
223
+
224
+ stage('Run GCP tests') {
225
+ when { expression { params.RUN_GCP_TESTS } }
226
+ environment {
227
+ INFRAPOOL_RUN_GCP_TESTS = 'true'
228
+ INFRAPOOL_RUBY_VERSION = '3.3'
229
+ INFRAPOOL_REGISTRY_URL = "registry.tld"
230
+ GCP_CTX_DIR = "gcp-ctx"
231
+ }
232
+ steps {
233
+ script {
234
+ // GCP agents lack a Docker runtime, so fetch a real identity token on
235
+ // the GCP VM, stash it, then run the actual test on the AWS agent with
236
+ // the token supplied via GCP_ID_TOKEN.
237
+ gcpInfrapool.agentSh "./ci/get_gcp_token.sh data/test/gcp-apps/test-app cucumber ${GCP_CTX_DIR}"
238
+ gcpInfrapool.agentStash name: 'gcp-token', includes: "${GCP_CTX_DIR}/*"
239
+ infrapool.agentUnstash name: 'gcp-token'
240
+ infrapool.agentSh """
241
+ export GCP_ID_TOKEN="\$(cat ${GCP_CTX_DIR}/token)"
242
+ export GCP_PROJECT_ID="\$(cat ${GCP_CTX_DIR}/project-id)"
243
+ ./test.sh
244
+ """
245
+ }
246
+ }
247
+ post {
248
+ always {
249
+ script { infrapool.agentStash name: 'reports-gcp', includes: '**/reports/*.xml' }
250
+ unstash 'reports-gcp'
141
251
  }
252
+ }
253
+ }
254
+
255
+ stage('Submit Coverage Report'){
256
+ steps{
257
+ // Coverage was stashed at the end of 'Test Ruby 4.0', before the cloud
258
+ // stages regenerated a partial coverage/ on the agent.
142
259
  unstash 'coverage'
143
260
 
144
261
  recordCoverage(
145
262
  tools: [[parser: 'COBERTURA', pattern: 'coverage/coverage.xml']],
146
- sourceCodeEncoding: 'ASCII',
263
+ sourceCodeEncoding: 'UTF-8',
147
264
  qualityGates: [
148
265
  [threshold: 70.0, metric: 'LINE', baseline: 'PROJECT', unstable: true],
149
266
  [threshold: 70.0, metric: 'BRANCH', baseline: 'PROJECT', unstable: true],
data/Rakefile CHANGED
@@ -23,8 +23,34 @@ begin
23
23
  require 'cucumber'
24
24
  require 'cucumber/rake/task'
25
25
 
26
+ # Cloud-authenticator features (authn-iam / authn-azure / authn-jwt gcp) are
27
+ # live integration tests that only work on the matching cloud agent, so they
28
+ # are excluded from the default suite and opted into via the CLOUD_AUTHN_TAG
29
+ # env var by the dedicated jenkins_cucumber_cloud task.
30
+ CLOUD_AUTHN_TAGS = %w[@authn_iam @authn_azure @authn_gcp].freeze
31
+
26
32
  Cucumber::Rake::Task.new(:cucumber) do |t|
27
- t.cucumber_opts = "--tags 'not @wip' --format pretty --format junit --out features/reports"
33
+ exclude = (['@wip'] + CLOUD_AUTHN_TAGS).map { |tag| "not #{tag}" }.join(' and ')
34
+ t.cucumber_opts = "--tags '#{exclude}' --format pretty --format junit --out features/reports"
35
+ end
36
+
37
+ # Runs a single cloud-authenticator feature, selected by CLOUD_AUTHN_TAG
38
+ # (e.g. CLOUD_AUTHN_TAG=@authn_iam). Used by the cloud CI stages.
39
+ #
40
+ # NOTE: this block runs at Rakefile load time, so it must not raise when
41
+ # CLOUD_AUTHN_TAG is unset (that would break every other rake invocation).
42
+ # The value is validated in the :validate_cloud_authn_tag prerequisite,
43
+ # which only runs when this task is actually invoked. The fallback tag here
44
+ # is only a placeholder for the load-time cucumber_opts string.
45
+ Cucumber::Rake::Task.new(:cucumber_cloud) do |t|
46
+ tag = ENV['CLOUD_AUTHN_TAG'].to_s.empty? ? '@authn_iam' : ENV['CLOUD_AUTHN_TAG']
47
+ t.cucumber_opts = "--tags '#{tag}' --format pretty --format junit --out features/reports"
48
+ end
49
+
50
+ task :validate_cloud_authn_tag do
51
+ if ENV['CLOUD_AUTHN_TAG'].to_s.empty?
52
+ raise 'CLOUD_AUTHN_TAG must be set (e.g. @authn_iam) to run jenkins_cucumber_cloud'
53
+ end
28
54
  end
29
55
 
30
56
  begin
@@ -33,6 +59,7 @@ begin
33
59
  task :jenkins_init => [ :init_coverage, :cuke_report_cleanup ]
34
60
  task :jenkins_spec => [ :"ci:setup:rspec", :spec ]
35
61
  task :jenkins_cucumber => [ :cucumber ]
62
+ task :jenkins_cucumber_cloud => [ :validate_cloud_authn_tag, :cucumber_cloud ]
36
63
  rescue LoadError
37
64
  warn "ci_reporter_rspec not found, jenkins task will be unavailable"
38
65
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 6.3.1-846
1
+ 6.3.1-848
@@ -0,0 +1,43 @@
1
+ #!/bin/bash -e
2
+ #
3
+ # Fetch a GCP identity token from the instance metadata service.
4
+ #
5
+ # This is meant to run ON a GCP executor agent. GCP agents typically lack a
6
+ # Docker runtime, so the authn-jwt/gcp integration test itself runs on a
7
+ # different (e.g. AWS) agent. This script fetches a real identity token here,
8
+ # writes it to a file, and the Jenkins pipeline stashes it and hands it to the
9
+ # test container via the GCP_ID_TOKEN env var.
10
+ #
11
+ # Usage: ./ci/get_gcp_token.sh <host-id> <account> <output-dir>
12
+ # host-id Conjur host id WITHOUT the "host/" prefix
13
+ # (e.g. "data/test/gcp-apps/test-app")
14
+ # account Conjur account (e.g. "cucumber")
15
+ # output-dir directory to write "token" and "project-id" into
16
+
17
+ HOST_ID="${1:?host-id (without host/ prefix) is required}"
18
+ ACCOUNT="${2:?account is required}"
19
+ OUT_DIR="${3:?output directory is required}"
20
+
21
+ METADATA_BASE="http://metadata.google.internal/computeMetadata/v1"
22
+ AUDIENCE="conjur/${ACCOUNT}/host/${HOST_ID}"
23
+
24
+ mkdir -p "${OUT_DIR}"
25
+
26
+ echo "Fetching GCP identity token for audience: ${AUDIENCE}"
27
+ token="$(curl -sS --noproxy '*' -H 'Metadata-Flavor: Google' \
28
+ "${METADATA_BASE}/instance/service-accounts/default/identity?audience=${AUDIENCE}&format=full")"
29
+
30
+ # Sanity-check that we got a 3-part JWT rather than an HTML error page.
31
+ if [[ "$(grep -o '\.' <<< "${token}" | wc -l | tr -d ' ')" != "2" ]]; then
32
+ echo "ERROR: metadata service did not return a JWT. Got:" >&2
33
+ echo "${token}" >&2
34
+ exit 1
35
+ fi
36
+
37
+ project_id="$(curl -sS --noproxy '*' -H 'Metadata-Flavor: Google' \
38
+ "${METADATA_BASE}/project/project-id")"
39
+
40
+ printf '%s' "${token}" > "${OUT_DIR}/token"
41
+ printf '%s' "${project_id}" > "${OUT_DIR}/project-id"
42
+
43
+ echo "Wrote ${OUT_DIR}/token and ${OUT_DIR}/project-id"
data/conjur-api.gemspec CHANGED
@@ -40,4 +40,8 @@ Gem::Specification.new do |gem|
40
40
  gem.add_development_dependency 'pry-byebug'
41
41
  gem.add_development_dependency 'nokogiri'
42
42
  gem.add_development_dependency 'webrick'
43
+ # Optional runtime dependency for the IAM authenticator (authn-iam); not a
44
+ # hard dependency of the gem, but required to exercise it in the integration
45
+ # tests.
46
+ gem.add_development_dependency 'aws-sdk-core'
43
47
  end
data/docker-compose.yml CHANGED
@@ -11,6 +11,14 @@ services:
11
11
  environment:
12
12
  DATABASE_URL: postgres://postgres@pg/postgres
13
13
  CONJUR_DATA_KEY: 'WMfApcDBtocRWV+ZSUP3Tjr5XNU+Z2FdBb6BEezejIs='
14
+ # NOTE: CONJUR_AUTHENTICATORS is intentionally left unset. When unset the
15
+ # server is permissive, so both the existing authn-oidc tests and the
16
+ # cloud-authenticator features can enable their authenticators at runtime
17
+ # via authenticator_enable. Setting an explicit allowlist here makes the
18
+ # server strict and breaks the authn-oidc scenarios, and any authenticators
19
+ # listed would show up in authenticator_list's "enabled" set (which the
20
+ # authenticators.feature assertions depend on staying ["authn"]).
21
+ #
14
22
  # Conjur's SSRF guard blocks authenticator fetches (authn-oidc provider-uri here) that
15
23
  # resolve to loopback/RFC1918 ranges or use plain HTTP. Keycloak is reached by compose
16
24
  # service name, so it resolves inside the bridge network's private subnet and its
@@ -0,0 +1,80 @@
1
+ # Cucumber integration features
2
+
3
+ The default suite (run by `rake jenkins_cucumber` via `test.sh`) exercises the
4
+ API against a local Conjur brought up by `docker-compose`.
5
+
6
+ ## Cloud authenticator features
7
+
8
+ `authn_iam.feature`, `authn_azure.feature`, and `authn_gcp.feature` are **live
9
+ integration tests** for the AWS IAM / Azure / GCP authenticators. They are
10
+ tagged `@authn_iam` / `@authn_azure` / `@authn_gcp` and are **excluded from the
11
+ default run** (see `Rakefile`), because they only pass on the matching cloud
12
+ provider's infrastructure.
13
+
14
+ ### How they run in CI
15
+
16
+ They are gated behind Jenkins boolean params `RUN_AWS_TESTS` / `RUN_AZURE_TESTS`
17
+ / `RUN_GCP_TESTS` (default `false`; enabled by the daily cron). When a flag is
18
+ set, `test.sh` runs only that authenticator's feature against a real Conjur
19
+ (with `CONJUR_AUTHENTICATORS` including the cloud authenticators) plus the real
20
+ cloud metadata/STS service:
21
+
22
+ - **AWS IAM** — runs on the standard `ExecutorV2` agent, which is itself in AWS
23
+ and carries an instance profile. The stage derives the caller identity with
24
+ `aws sts get-caller-identity` and passes `AWS_ACCOUNT_ID` / `AWS_ROLE_NAME`;
25
+ the Conjur host id is `<account-id>/<role-name>` (authn-iam matches the host
26
+ to the signing role). `new_from_iam` signs a real STS `GetCallerIdentity`
27
+ request.
28
+ - **Azure** — runs on an `AzureExecutorV2` agent. `summon -e azure` injects
29
+ `AZURE_SUBSCRIPTION_ID`, `AZURE_RESOURCE_GROUP`, and
30
+ `USER_ASSIGNED_IDENTITY_CLIENT_ID`. `new_from_azure` fetches a JWT from the
31
+ real Azure IMDS (`169.254.169.254`); the feature derives the `provider-uri`
32
+ from that token's `iss` claim and annotates the host with the
33
+ subscription-id / resource-group restrictions.
34
+ - **GCP** — GCP agents lack a Docker runtime, so the flow is split:
35
+ `ci/get_gcp_token.sh` fetches a real identity token on a `GcpExecutorV2`
36
+ agent, Jenkins stashes it, and the test runs on the AWS agent with the token
37
+ passed via `GCP_ID_TOKEN` (and `GCP_PROJECT_ID` for the host's
38
+ `authn-gcp/project-id` annotation). `new_from_gcp` exchanges that pre-fetched
39
+ token via the serviceless `authn-gcp` endpoint, bypassing the metadata service.
40
+
41
+ `test.sh` forwards the cloud env vars into the tester container and clears
42
+ `HTTP(S)_PROXY` / sets `NO_PROXY` so the link-local metadata endpoints are
43
+ reachable directly.
44
+
45
+ ### Running one locally
46
+
47
+ You must be on (or have credentials for) the relevant cloud. For example, on an
48
+ AWS host with an appropriate instance profile / credentials:
49
+
50
+ ```sh
51
+ RUN_AWS_TESTS=true ./test.sh
52
+ ```
53
+
54
+ For GCP, fetch a token first (on a GCP host), then point the test at it:
55
+
56
+ ```sh
57
+ ./ci/get_gcp_token.sh data/test/gcp-apps/test-app cucumber gcp-ctx
58
+ GCP_ID_TOKEN="$(cat gcp-ctx/token)" RUN_GCP_TESTS=true ./test.sh
59
+ ```
60
+
61
+ ### CI infrastructure these depend on
62
+
63
+ The cloud stages authenticate against real provider infrastructure, so they
64
+ rely on the following being in place (all currently provisioned in the
65
+ Conjur-Enterprise CI):
66
+
67
+ 1. `AzureExecutorV2` / `GcpExecutorV2` InfraPool agent types, plus the standard
68
+ AWS `ExecutorV2` agent (used for AWS IAM and to run the GCP test).
69
+ 2. The `!var` references in `secrets.yml` (`ci/azure/...`) resolvable from the
70
+ CI Conjur vault, so `summon -e azure` can supply the `AZURE_*` /
71
+ `USER_ASSIGNED_IDENTITY_CLIENT_ID` values.
72
+ 3. Cloud identities matching how each feature builds its Conjur host:
73
+ - **AWS**: the executor's instance-profile role (host id `<account>/<role>`,
74
+ derived at runtime).
75
+ - **Azure**: the managed identity in the given subscription / resource-group.
76
+ - **GCP**: the `GcpExecutorV2` agent's service account, mapped to
77
+ `data/test/gcp-apps/test-app` via the `authn-gcp/project-id` annotation.
78
+
79
+ If any of these change, a stage may fail with an opaque `401`; `test.sh` dumps
80
+ the Conjur server log on cloud-test failure so the precise reason is visible.
@@ -0,0 +1,15 @@
1
+ @authn_azure
2
+ Feature: Authenticate to Conjur using Azure Managed Identity (authn-azure)
3
+
4
+ This scenario is a live integration test: it exercises new_from_azure against
5
+ the real Azure Instance Metadata Service (IMDS). It is tagged @authn_azure and
6
+ is only run in CI on an Azure executor agent, gated by RUN_AZURE_TESTS, which
7
+ also supplies AZURE_SUBSCRIPTION_ID / AZURE_RESOURCE_GROUP / the user-assigned
8
+ identity client id. See Jenkinsfile and features/README.md.
9
+
10
+ Scenario: Authenticate with a system-assigned identity and retrieve a secret
11
+ Given I enable the authn-azure authenticator "prod" for host "test/azure-apps/test-app" using Azure environment config
12
+ And the secret "test/azure-apps/db-password" has value "s3cr3t"
13
+ When I authenticate via authn-azure as "host/test/azure-apps/test-app" with service id "prod"
14
+ Then I receive a Conjur access token
15
+ And I can retrieve the secret "test/azure-apps/db-password" with value "s3cr3t"
@@ -0,0 +1,19 @@
1
+ @authn_gcp
2
+ Feature: Authenticate to Conjur using GCP workload identity (authn-gcp)
3
+
4
+ This scenario is a live integration test. GCP executor agents typically lack a
5
+ Docker runtime, so the flow is split: ci/get_gcp_token.sh fetches a real GCP
6
+ identity token on the GCP agent, the pipeline hands it to this test via
7
+ GCP_ID_TOKEN, and new_from_gcp exchanges that pre-fetched token with Conjur
8
+ (bypassing the metadata service). Tagged @authn_gcp, gated by RUN_GCP_TESTS.
9
+ See Jenkinsfile and features/README.md.
10
+
11
+ authn-gcp is a serviceless authenticator: the host is identified by its
12
+ authn-gcp/project-id annotation (set from GCP_PROJECT_ID), not a service id.
13
+
14
+ Scenario: Authenticate with a pre-fetched GCP identity token and retrieve a secret
15
+ Given I enable the authn-gcp authenticator for host "data/test/gcp-apps/test-app"
16
+ And the secret "data/test/gcp-apps/db-password" has value "s3cr3t"
17
+ When I authenticate via authn-gcp as "host/data/test/gcp-apps/test-app" using the pre-fetched token
18
+ Then I receive a Conjur access token
19
+ And I can retrieve the secret "data/test/gcp-apps/db-password" with value "s3cr3t"
@@ -0,0 +1,18 @@
1
+ @authn_iam
2
+ Feature: Authenticate to Conjur using AWS IAM (authn-iam)
3
+
4
+ This scenario is a live integration test: it exercises new_from_iam against
5
+ the real AWS STS service using the credentials of the host it runs on. It is
6
+ tagged @authn_iam and is only run in CI on an AWS executor agent, gated by
7
+ RUN_AWS_TESTS. See Jenkinsfile and features/README.md.
8
+
9
+ authn-iam identifies the Conjur host by the AWS caller identity, so the host
10
+ id is "<AWS_ACCOUNT_ID>/<AWS_ROLE_NAME>" (derived on the agent from
11
+ `aws sts get-caller-identity` and passed in via env).
12
+
13
+ Scenario: Authenticate and retrieve a secret via authn-iam
14
+ Given I enable the authn-iam authenticator "prod" for the AWS caller identity host
15
+ And the secret "test/iam-apps/db-password" has value "s3cr3t"
16
+ When I authenticate via authn-iam as the AWS caller identity host with service id "prod"
17
+ Then I receive a Conjur access token
18
+ And I can retrieve the secret "test/iam-apps/db-password" with value "s3cr3t"
@@ -0,0 +1,182 @@
1
+ require 'base64'
2
+ require 'json'
3
+
4
+ # Step definitions for the cloud authenticator integration features
5
+ # (authn-iam, authn-azure, authn-gcp).
6
+ #
7
+ # These steps drive real Conjur (brought up by docker-compose) against the
8
+ # cloud provider's real metadata/STS services. They are only meaningful when
9
+ # run on the matching cloud agent, so the features carry @authn_iam /
10
+ # @authn_azure / @authn_gcp tags and are gated by RUN_AWS_TESTS /
11
+ # RUN_AZURE_TESTS / RUN_GCP_TESTS in the pipeline.
12
+
13
+ # --- authn-iam -------------------------------------------------------------
14
+
15
+ # authn-iam matches the Conjur host to the AWS caller identity, so the host id
16
+ # must be "<account-id>/<role-name>" (supplied via env by the AWS CI stage).
17
+ def aws_caller_identity_host_id
18
+ account = ENV.fetch('AWS_ACCOUNT_ID')
19
+ role = ENV.fetch('AWS_ROLE_NAME')
20
+ "#{account}/#{role}"
21
+ end
22
+
23
+ Given(/^I enable the authn-iam authenticator "([^"]+)" for the AWS caller identity host$/) do |service_id|
24
+ host_id = aws_caller_identity_host_id
25
+ $conjur.load_policy 'root', <<-POLICY
26
+ - !policy
27
+ id: conjur/authn-iam/#{service_id}
28
+ body:
29
+ - !webservice
30
+ - !group apps
31
+ - !permit
32
+ role: !group apps
33
+ privilege: [ read, authenticate ]
34
+ resource: !webservice
35
+
36
+ - !host
37
+ id: #{host_id}
38
+ annotations:
39
+ authn-iam/#{service_id}: true
40
+
41
+ - !grant
42
+ role: !group conjur/authn-iam/#{service_id}/apps
43
+ member: !host #{host_id}
44
+ POLICY
45
+
46
+ @cloud_host_id = host_id
47
+ $conjur.authenticator_enable('authn-iam', service_id)
48
+ end
49
+
50
+ When(/^I authenticate via authn-iam as the AWS caller identity host with service id "([^"]+)"$/) do |service_id|
51
+ identity = "host/#{aws_caller_identity_host_id}"
52
+ @cloud_api = Conjur::API.new_from_iam(identity, service_id: service_id)
53
+ @result = @cloud_api.token
54
+ end
55
+
56
+ # --- authn-azure -----------------------------------------------------------
57
+
58
+ # The authn-azure provider-uri must equal the token's actual issuer
59
+ # (https://sts.windows.net/<tenant>/). Derive it from a real IMDS token's `iss`
60
+ # claim rather than hardcoding, matching conjur-api-java's resolveProviderUriFromImds.
61
+ def azure_provider_uri
62
+ jwt = Conjur::API.send(:fetch_azure_jwt,
63
+ resource_uri: Conjur::API::AzureAuthenticator::DEFAULT_RESOURCE,
64
+ client_id: (ENV['USER_ASSIGNED_IDENTITY_CLIENT_ID'] unless ENV['USER_ASSIGNED_IDENTITY_CLIENT_ID'].to_s.empty?),
65
+ imds_base_url: Conjur::API::AzureAuthenticator::DEFAULT_IMDS_BASE,
66
+ imds_api_version: Conjur::API::AzureAuthenticator::DEFAULT_IMDS_VER)
67
+ payload = JSON.parse(Base64.decode64(jwt.split('.')[1]))
68
+ issuer = payload.fetch('iss')
69
+ issuer.end_with?('/') ? issuer : "#{issuer}/"
70
+ end
71
+
72
+ Given(/^I enable the authn-azure authenticator "([^"]+)" for host "([^"]+)" using Azure environment config$/) do |service_id, host_id|
73
+ subscription = ENV.fetch('AZURE_SUBSCRIPTION_ID')
74
+ resource_group = ENV.fetch('AZURE_RESOURCE_GROUP')
75
+ provider_uri = azure_provider_uri
76
+
77
+ $conjur.load_policy 'root', <<-POLICY
78
+ - !policy
79
+ id: conjur/authn-azure/#{service_id}
80
+ body:
81
+ - !webservice
82
+ - !variable provider-uri
83
+ - !group apps
84
+ - !permit
85
+ role: !group apps
86
+ privilege: [ read, authenticate ]
87
+ resource: !webservice
88
+
89
+ - !host
90
+ id: #{host_id}
91
+ annotations:
92
+ authn-azure/subscription-id: "#{subscription}"
93
+ authn-azure/resource-group: "#{resource_group}"
94
+
95
+ - !grant
96
+ role: !group conjur/authn-azure/#{service_id}/apps
97
+ member: !host #{host_id}
98
+ POLICY
99
+
100
+ @cloud_host_id = host_id
101
+ $conjur.resource("cucumber:variable:conjur/authn-azure/#{service_id}/provider-uri")
102
+ .add_value(provider_uri)
103
+ $conjur.authenticator_enable('authn-azure', service_id)
104
+ end
105
+
106
+ When(/^I authenticate via authn-azure as "([^"]+)" with service id "([^"]+)"(?: and client id "([^"]*)")?$/) do |identity, service_id, client_id|
107
+ # new_from_azure takes service_id and identity positionally; client_id is a keyword.
108
+ opts = {}
109
+ opts[:client_id] = client_id if client_id && !client_id.empty?
110
+ @cloud_api = Conjur::API.new_from_azure(service_id, identity, **opts)
111
+ @result = @cloud_api.token
112
+ end
113
+
114
+ # --- authn-gcp -------------------------------------------------------------
115
+
116
+ Given(/^I enable the authn-gcp authenticator for host "([^"]+)"$/) do |host_id|
117
+ # authn-gcp is serviceless. The host is matched to the GCP identity token via
118
+ # its authn-gcp/project-id annotation, so the project id must be the one the
119
+ # token was issued for (supplied by ci/get_gcp_token.sh as GCP_PROJECT_ID).
120
+ project_id = ENV.fetch('GCP_PROJECT_ID')
121
+
122
+ $conjur.load_policy 'root', <<-POLICY
123
+ - !policy
124
+ id: conjur/authn-gcp
125
+ body:
126
+ - !webservice
127
+ - !group apps
128
+ - !permit
129
+ role: !group apps
130
+ privilege: [ read, authenticate ]
131
+ resource: !webservice
132
+
133
+ - !host
134
+ id: #{host_id}
135
+ annotations:
136
+ authn-gcp/project-id: "#{project_id}"
137
+
138
+ - !grant
139
+ role: !group conjur/authn-gcp/apps
140
+ member: !host #{host_id}
141
+ POLICY
142
+
143
+ @cloud_host_id = host_id
144
+ $conjur.authenticator_enable('authn-gcp', '')
145
+ end
146
+
147
+ When(/^I authenticate via authn-gcp as "([^"]+)" using the pre-fetched token$/) do |identity|
148
+ jwt = ENV['GCP_ID_TOKEN']
149
+ raise 'GCP_ID_TOKEN is not set; run ci/get_gcp_token.sh on a GCP agent first' if jwt.nil? || jwt.empty?
150
+
151
+ @cloud_api = Conjur::API.new_from_gcp(identity, jwt: jwt)
152
+ @result = @cloud_api.token
153
+ end
154
+
155
+ # --- shared assertions -----------------------------------------------------
156
+
157
+ Then(/^I receive a Conjur access token$/) do
158
+ # Conjur access tokens are returned parsed into a Hash of the JWT parts.
159
+ expect(@result).to be_a(Hash)
160
+ expect(@result).to include('protected', 'payload', 'signature')
161
+ end
162
+
163
+ Given(/^the secret "([^"]+)" has value "([^"]+)"$/) do |var_id, value|
164
+ # Declare the variable and grant the cloud host permission to read it, then
165
+ # set the value. @cloud_host_id is captured by the "enable ... authenticator"
166
+ # step that always runs first.
167
+ raise 'no cloud host configured; run the "enable authenticator" step first' unless @cloud_host_id
168
+
169
+ $conjur.load_policy 'root', <<-POLICY
170
+ - !variable #{var_id}
171
+ - !permit
172
+ role: !host #{@cloud_host_id}
173
+ privilege: [ read, execute ]
174
+ resource: !variable #{var_id}
175
+ POLICY
176
+
177
+ $conjur.resource("cucumber:variable:#{var_id}").add_value(value)
178
+ end
179
+
180
+ Then(/^I can retrieve the secret "([^"]+)" with value "([^"]+)"$/) do |var_id, value|
181
+ expect(@cloud_api.resource("cucumber:variable:#{var_id}").value).to eq(value)
182
+ end
@@ -9,94 +9,82 @@ module Conjur
9
9
  class API
10
10
  # Authenticator that uses a GCP identity token to obtain Conjur access tokens.
11
11
  # Fetches an identity JWT from the GCP Instance Metadata Service, then POSTs it
12
- # to the Conjur authn-jwt/gcp endpoint.
12
+ # to the Conjur authn-gcp endpoint.
13
+ #
14
+ # authn-gcp is a serviceless authenticator: there is no service ID, and Conjur
15
+ # identifies the calling host from the token's claims (matched against the
16
+ # host's authn-gcp/project-id annotation), so the host id is not sent in the
17
+ # authenticate URL.
13
18
  #
14
19
  # An explicit JWT may be supplied to bypass the metadata service fetch —
15
20
  # useful in CI environments where the token is pre-fetched on a GCP agent.
16
21
  class GCPAuthenticator
17
22
  include TokenExpiration
18
23
 
19
- DEFAULT_SERVICE_ID = 'gcp'.freeze
20
24
  DEFAULT_METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1' \
21
25
  '/instance/service-accounts/default/identity'.freeze
22
26
 
23
- attr_reader :account, :service_id, :identity, :jwt, :gcp_identity_url, :audience
27
+ attr_reader :account, :identity, :jwt, :gcp_identity_url
24
28
 
25
- def initialize(account, service_id, identity, jwt: nil,
26
- gcp_identity_url: DEFAULT_METADATA_URL, audience: nil)
29
+ def initialize(account, identity, jwt: nil,
30
+ gcp_identity_url: DEFAULT_METADATA_URL)
27
31
  @account = account
28
- @service_id = service_id
29
32
  @identity = identity
30
33
  @jwt = jwt
31
34
  @gcp_identity_url = gcp_identity_url
32
- @audience = audience
33
35
  update_token_born
34
36
  end
35
37
 
36
38
  def refresh_token
37
39
  Conjur::API.authenticate_gcp(identity, account: account,
38
- service_id: service_id,
39
40
  jwt: jwt,
40
- gcp_identity_url: gcp_identity_url,
41
- audience: audience).tap do
41
+ gcp_identity_url: gcp_identity_url).tap do
42
42
  update_token_born
43
43
  end
44
44
  end
45
45
  end
46
46
 
47
47
  class << self
48
- # Create a {Conjur::API} instance authenticated via GCP workload identity (authn-jwt/gcp).
48
+ # Create a {Conjur::API} instance authenticated via GCP workload identity (authn-gcp).
49
49
  #
50
- # @param [String] identity Conjur host identity (e.g. "host/data/test/gcp-apps/test-app")
51
- # @param [String] service_id the authn-jwt service ID (default: "gcp")
50
+ # @param [String] identity Conjur host identity (e.g. "host/data/test/gcp-apps/test-app").
51
+ # Used to build the metadata-service audience; Conjur itself derives the
52
+ # host from the token, so it is not sent in the authenticate request.
52
53
  # @param [String] account the Conjur organization account
53
54
  # @param [String, nil] jwt pre-obtained GCP identity token; when supplied the metadata
54
55
  # service is not called. Useful in tests and CI where the token is pre-fetched.
55
56
  # @param [String] gcp_identity_url override for the GCP metadata identity URL (for tests)
56
- # @param [String, nil] audience the audience claim requested from the metadata service.
57
- # Must match the audience configured in the authn-jwt/gcp policy. Defaults to
58
- # "conjur/{account}/host/{identity}" when not supplied.
59
57
  # @param [String, nil] remote_ip optional IP address recorded in the audit log
60
58
  # @return [Conjur::API]
61
59
  def new_from_gcp(identity,
62
60
  account: Conjur.configuration.account,
63
- service_id: GCPAuthenticator::DEFAULT_SERVICE_ID,
64
61
  jwt: nil,
65
62
  gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL,
66
- audience: nil,
67
63
  remote_ip: nil)
68
- self.new.init_from_gcp(identity, account: account, service_id: service_id,
69
- jwt: jwt, gcp_identity_url: gcp_identity_url,
70
- audience: audience, remote_ip: remote_ip)
64
+ self.new.init_from_gcp(identity, account: account,
65
+ jwt: jwt, gcp_identity_url: gcp_identity_url,
66
+ remote_ip: remote_ip)
71
67
  end
72
68
 
73
- # Authenticate via authn-jwt/gcp and return a parsed Conjur access token.
69
+ # Authenticate via authn-gcp and return a parsed Conjur access token.
74
70
  #
75
- # @param [String] identity Conjur host identity
76
- # @param [String] service_id the authn-jwt service ID
71
+ # @param [String] identity Conjur host identity (used to build the audience)
77
72
  # @param [String] account the Conjur organization account
78
73
  # @param [String, nil] jwt pre-obtained GCP identity token
79
74
  # @param [String] gcp_identity_url override for the GCP metadata identity URL
80
- # @param [String, nil] audience the audience claim requested from the metadata service.
81
- # Must match the audience configured in the authn-jwt/gcp policy. Defaults to
82
- # "conjur/{account}/host/{identity}" when not supplied.
83
75
  # @return [Hash] parsed access token
84
76
  def authenticate_gcp(identity,
85
77
  account: Conjur.configuration.account,
86
- service_id: GCPAuthenticator::DEFAULT_SERVICE_ID,
87
78
  jwt: nil,
88
- gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL,
89
- audience: nil)
90
- raise ArgumentError, "identity is required" if identity.nil? || identity.empty?
91
- raise ArgumentError, "service_id is required" if service_id.nil? || service_id.empty?
79
+ gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL)
80
+ raise ArgumentError, "identity is required" if identity.nil? || identity.empty?
92
81
 
93
82
  if Conjur.log
94
- Conjur.log << "Authenticating #{identity} to account #{account} via authn-jwt/#{service_id}\n"
83
+ Conjur.log << "Authenticating #{identity} to account #{account} via authn-gcp\n"
95
84
  end
96
85
 
97
- token = jwt || fetch_gcp_jwt(account, identity,
98
- gcp_identity_url: gcp_identity_url, audience: audience)
99
- JSON.parse(url_for(:authn_gcp_authenticate, account, service_id, identity)
86
+ token = jwt || fetch_gcp_jwt(account, identity, gcp_identity_url: gcp_identity_url)
87
+ JSON.parse(url_for(:authn_gcp_authenticate, account)
100
88
  .post("jwt=#{CGI.escape(token)}", content_type: 'application/x-www-form-urlencoded'))
101
89
  end
102
90
 
@@ -106,11 +94,10 @@ module Conjur
106
94
  # Uses a direct Net::HTTP connection (proxy addr = nil) because the metadata
107
95
  # link-local address must never be routed through a proxy.
108
96
  #
109
- # When +audience+ is nil the default "conjur/{account}/host/{identity}" is used,
110
- # matching the conventional authn-jwt/gcp policy. Pass an explicit audience to
111
- # match a policy configured with a different value.
112
- def fetch_gcp_jwt(account, identity, gcp_identity_url:, audience: nil)
113
- aud = audience || default_gcp_audience(account, identity)
97
+ # The requested audience is "conjur/{account}/host/{identity}", matching what
98
+ # the authn-gcp authenticator expects.
99
+ def fetch_gcp_jwt(account, identity, gcp_identity_url:)
100
+ aud = gcp_audience(account, identity)
114
101
  url = "#{gcp_identity_url}?#{URI.encode_www_form(audience: aud, format: 'full')}"
115
102
 
116
103
  uri = URI(url)
@@ -127,10 +114,10 @@ module Conjur
127
114
  token
128
115
  end
129
116
 
130
- # Builds the conventional authn-jwt/gcp audience for a Conjur host identity,
131
- # e.g. "conjur/myorg/host/data/app". The "host/" prefix is optional in the
117
+ # Builds the authn-gcp audience for a Conjur host identity, e.g.
118
+ # "conjur/myorg/host/data/app". The "host/" prefix is optional in the
132
119
  # supplied identity and is normalized away before being re-applied.
133
- def default_gcp_audience(account, identity)
120
+ def gcp_audience(account, identity)
134
121
  normalized = identity.start_with?('host/') ? identity['host/'.length..] : identity
135
122
  "conjur/#{account}/host/#{normalized}"
136
123
  end
@@ -138,15 +125,12 @@ module Conjur
138
125
 
139
126
  def init_from_gcp(identity,
140
127
  account: Conjur.configuration.account,
141
- service_id: GCPAuthenticator::DEFAULT_SERVICE_ID,
142
128
  jwt: nil,
143
129
  gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL,
144
- audience: nil,
145
130
  remote_ip: nil)
146
131
  @remote_ip = remote_ip
147
- @authenticator = GCPAuthenticator.new(account, service_id, identity,
148
- jwt: jwt, gcp_identity_url: gcp_identity_url,
149
- audience: audience)
132
+ @authenticator = GCPAuthenticator.new(account, identity,
133
+ jwt: jwt, gcp_identity_url: gcp_identity_url)
150
134
  self
151
135
  end
152
136
  end
@@ -58,12 +58,15 @@ module Conjur
58
58
  )['authn-iam'][fully_escape service_id][fully_escape account][fully_escape identity]['authenticate']
59
59
  end
60
60
 
61
- # POST /authn-jwt/{service_id}/{account}/{login}/authenticate
62
- def authn_gcp_authenticate account, service_id, identity
61
+ # POST /authn-gcp/{account}/authenticate
62
+ # authn-gcp is a serviceless authenticator: Conjur derives the host from
63
+ # the GCP identity token's claims, so neither a service id nor the login
64
+ # appears in the path.
65
+ def authn_gcp_authenticate account
63
66
  RestClient::Resource.new(
64
67
  Conjur.configuration.core_url,
65
68
  Conjur.configuration.rest_client_options
66
- )['authn-jwt'][fully_escape service_id][fully_escape account][fully_escape identity]['authenticate']
69
+ )['authn-gcp'][fully_escape account]['authenticate']
67
70
  end
68
71
 
69
72
  # Builds the RestClient::Resource for an authn-cert authentication request.
@@ -86,10 +89,14 @@ module Conjur
86
89
  end
87
90
 
88
91
  def authenticator account, authenticator, service_id, credentials
89
- RestClient::Resource.new(
92
+ resource = RestClient::Resource.new(
90
93
  Conjur.configuration.core_url,
91
94
  Conjur.configuration.create_rest_client_options(credentials)
92
- )[fully_escape authenticator][fully_escape service_id][fully_escape account]
95
+ )[fully_escape authenticator]
96
+ # Serviceless authenticators (e.g. authn-gcp) have no service id segment:
97
+ # the path is authn-<type>/<account>.
98
+ resource = resource[fully_escape service_id] unless service_id.nil? || service_id.empty?
99
+ resource[fully_escape account]
93
100
  end
94
101
 
95
102
  def authenticators
data/secrets.yml ADDED
@@ -0,0 +1,13 @@
1
+ # Summon secrets for CI. `summon -e <env> ./test.sh` resolves the !var
2
+ # references below from the CI Conjur vault and exports them into the
3
+ # environment for the wrapped command.
4
+ #
5
+ # The azure environment supplies the values the authn-azure integration
6
+ # feature needs (see features/authn_azure.feature and features/README.md).
7
+ # Mirrors the layout used by conjur-api-java / conjur-api-dotnet.
8
+ azure:
9
+ RUN_AZURE_TESTS: "true"
10
+ AZURE_SUBSCRIPTION_ID: !var ci/azure/subscription-id
11
+ AZURE_RESOURCE_GROUP: !var ci/azure/authn-test/resource-group
12
+ USER_ASSIGNED_IDENTITY: !var ci/azure/authn-test/user-assigned-id
13
+ USER_ASSIGNED_IDENTITY_CLIENT_ID: !var ci/azure/authn-test/user-assigned-id-client-id
@@ -5,7 +5,6 @@ require 'cgi'
5
5
  require 'conjur/api/router'
6
6
 
7
7
  describe Conjur::API, api: :dummy do
8
- let(:service_id) { 'gcp' }
9
8
  let(:identity) { 'host/data/test/gcp-apps/test-app' }
10
9
  let(:gcp_jwt) { 'eyJhbGciOiJSUzI1NiJ9.gcp-payload.sig' }
11
10
  let(:raw_token) { { 'protected' => 'p', 'payload' => 'pl', 'signature' => 's' } }
@@ -24,7 +23,7 @@ describe Conjur::API, api: :dummy do
24
23
 
25
24
  before do
26
25
  allow(Conjur::API).to receive(:url_for)
27
- .with(:authn_gcp_authenticate, account, service_id, identity)
26
+ .with(:authn_gcp_authenticate, account)
28
27
  .and_return(resource)
29
28
  allow(resource).to receive(:post)
30
29
  .with("jwt=#{CGI.escape(gcp_jwt)}", content_type: 'application/x-www-form-urlencoded')
@@ -34,9 +33,7 @@ describe Conjur::API, api: :dummy do
34
33
  context 'with a pre-supplied JWT' do
35
34
  it 'returns a parsed token without calling the metadata service' do
36
35
  expect(Conjur::API).not_to receive(:fetch_gcp_jwt)
37
- token = Conjur::API.authenticate_gcp(identity, account: account,
38
- service_id: service_id,
39
- jwt: gcp_jwt)
36
+ token = Conjur::API.authenticate_gcp(identity, account: account, jwt: gcp_jwt)
40
37
  expect(token).to eq(raw_token)
41
38
  end
42
39
  end
@@ -44,7 +41,7 @@ describe Conjur::API, api: :dummy do
44
41
  context 'without a pre-supplied JWT' do
45
42
  it 'fetches from the metadata service' do
46
43
  allow(Conjur::API).to receive(:fetch_gcp_jwt).and_return(gcp_jwt)
47
- token = Conjur::API.authenticate_gcp(identity, account: account, service_id: service_id)
44
+ token = Conjur::API.authenticate_gcp(identity, account: account)
48
45
  expect(token).to eq(raw_token)
49
46
  end
50
47
  end
@@ -53,27 +50,20 @@ describe Conjur::API, api: :dummy do
53
50
  expect { Conjur::API.authenticate_gcp('', account: account) }
54
51
  .to raise_error(ArgumentError, /identity is required/)
55
52
  end
56
-
57
- it 'raises ArgumentError when service_id is empty' do
58
- expect { Conjur::API.authenticate_gcp(identity, account: account, service_id: '') }
59
- .to raise_error(ArgumentError, /service_id is required/)
60
- end
61
53
  end
62
54
 
63
55
  describe 'GCPAuthenticator#refresh_token' do
64
56
  context 'with a pre-supplied JWT' do
65
57
  subject(:authenticator) do
66
- Conjur::API::GCPAuthenticator.new(account, service_id, identity, jwt: gcp_jwt)
58
+ Conjur::API::GCPAuthenticator.new(account, identity, jwt: gcp_jwt)
67
59
  end
68
60
 
69
61
  it 'passes the stored JWT to authenticate_gcp' do
70
62
  expect(Conjur::API).to receive(:authenticate_gcp)
71
63
  .with(identity,
72
64
  account: account,
73
- service_id: service_id,
74
65
  jwt: gcp_jwt,
75
- gcp_identity_url: Conjur::API::GCPAuthenticator::DEFAULT_METADATA_URL,
76
- audience: nil)
66
+ gcp_identity_url: Conjur::API::GCPAuthenticator::DEFAULT_METADATA_URL)
77
67
  .and_return(raw_token)
78
68
  expect(authenticator.refresh_token).to eq(raw_token)
79
69
  end
@@ -81,72 +71,29 @@ describe Conjur::API, api: :dummy do
81
71
 
82
72
  context 'without a pre-supplied JWT' do
83
73
  subject(:authenticator) do
84
- Conjur::API::GCPAuthenticator.new(account, service_id, identity)
74
+ Conjur::API::GCPAuthenticator.new(account, identity)
85
75
  end
86
76
 
87
77
  it 'passes nil jwt to authenticate_gcp (which then calls metadata service)' do
88
78
  expect(Conjur::API).to receive(:authenticate_gcp)
89
79
  .with(identity,
90
80
  account: account,
91
- service_id: service_id,
92
- jwt: nil,
93
- gcp_identity_url: Conjur::API::GCPAuthenticator::DEFAULT_METADATA_URL,
94
- audience: nil)
95
- .and_return(raw_token)
96
- expect(authenticator.refresh_token).to eq(raw_token)
97
- end
98
- end
99
-
100
- context 'with a custom audience' do
101
- subject(:authenticator) do
102
- Conjur::API::GCPAuthenticator.new(account, service_id, identity,
103
- audience: 'my-custom-audience')
104
- end
105
-
106
- it 'passes the stored audience to authenticate_gcp' do
107
- expect(Conjur::API).to receive(:authenticate_gcp)
108
- .with(identity,
109
- account: account,
110
- service_id: service_id,
111
81
  jwt: nil,
112
- gcp_identity_url: Conjur::API::GCPAuthenticator::DEFAULT_METADATA_URL,
113
- audience: 'my-custom-audience')
82
+ gcp_identity_url: Conjur::API::GCPAuthenticator::DEFAULT_METADATA_URL)
114
83
  .and_return(raw_token)
115
84
  expect(authenticator.refresh_token).to eq(raw_token)
116
85
  end
117
86
  end
118
87
  end
119
88
 
120
- describe 'GCPAuthenticator audience building' do
121
- it 'passes the identity and nil audience to fetch_gcp_jwt by default' do
122
- allow(Conjur::API).to receive(:url_for).and_return(double(post: raw_token.to_json))
123
- allow(Conjur::API).to receive(:fetch_gcp_jwt) do |acct, ident, gcp_identity_url:, audience:|
124
- expect(ident).to eq(identity)
125
- expect(audience).to be_nil
126
- gcp_jwt
127
- end
128
- Conjur::API.authenticate_gcp(identity, account: account, service_id: service_id)
129
- end
130
-
131
- it 'forwards an explicit audience to fetch_gcp_jwt' do
132
- allow(Conjur::API).to receive(:url_for).and_return(double(post: raw_token.to_json))
133
- allow(Conjur::API).to receive(:fetch_gcp_jwt) do |acct, ident, gcp_identity_url:, audience:|
134
- expect(audience).to eq('my-custom-audience')
135
- gcp_jwt
136
- end
137
- Conjur::API.authenticate_gcp(identity, account: account, service_id: service_id,
138
- audience: 'my-custom-audience')
139
- end
140
- end
141
-
142
- describe '.default_gcp_audience' do
89
+ describe '.gcp_audience' do
143
90
  it 'strips a leading host/ and rebuilds the conjur audience' do
144
- expect(Conjur::API.send(:default_gcp_audience, account, 'host/data/app'))
91
+ expect(Conjur::API.send(:gcp_audience, account, 'host/data/app'))
145
92
  .to eq("conjur/#{account}/host/data/app")
146
93
  end
147
94
 
148
95
  it 'assumes a host identity when the host/ prefix is absent' do
149
- expect(Conjur::API.send(:default_gcp_audience, account, 'data/app'))
96
+ expect(Conjur::API.send(:gcp_audience, account, 'data/app'))
150
97
  .to eq("conjur/#{account}/host/data/app")
151
98
  end
152
99
  end
@@ -154,8 +101,6 @@ end
154
101
 
155
102
  describe Conjur::API::Router do
156
103
  let(:account) { 'myaccount' }
157
- let(:service_id) { 'gcp' }
158
- let(:identity) { 'host/data/test/gcp-apps/test-app' }
159
104
 
160
105
  before do
161
106
  allow(Conjur.configuration).to receive(:core_url).and_return('https://conjur.example.com')
@@ -163,14 +108,14 @@ describe Conjur::API::Router do
163
108
  end
164
109
 
165
110
  describe '#authn_gcp_authenticate' do
166
- subject { described_class.authn_gcp_authenticate(account, service_id, identity) }
111
+ subject { described_class.authn_gcp_authenticate(account) }
167
112
 
168
113
  it 'returns a RestClient::Resource' do
169
114
  expect(subject).to be_a(RestClient::Resource)
170
115
  end
171
116
 
172
- it 'builds the correct URL using authn-jwt path' do
173
- expect(subject.url).to end_with('/authn-jwt/gcp/myaccount/host%2Fdata%2Ftest%2Fgcp-apps%2Ftest-app/authenticate')
117
+ it 'builds the correct URL using the serviceless authn-gcp path' do
118
+ expect(subject.url).to end_with('/authn-gcp/myaccount/authenticate')
174
119
  end
175
120
  end
176
121
  end
data/test.sh CHANGED
@@ -7,6 +7,22 @@ RUBY_VERSION="$(cut -d '-' -f 2 <<< "$RUBY_VERSION")"
7
7
  export REGISTRY_URL=${INFRAPOOL_REGISTRY_URL:-"docker.io"}
8
8
  export RUBY_VERSION="${INFRAPOOL_RUBY_VERSION:-$RUBY_VERSION}"
9
9
 
10
+ # Cloud-authenticator integration toggles. These accept both plain and
11
+ # INFRAPOOL_-prefixed variants (Jenkins injects the latter). When any is
12
+ # "true", ONLY that authenticator's tagged feature runs against real cloud
13
+ # infrastructure; the normal suite is skipped. Left unset by default so PR
14
+ # builds run the ordinary unit + cucumber suite.
15
+ RUN_AWS_TESTS="${INFRAPOOL_RUN_AWS_TESTS:-${RUN_AWS_TESTS:-false}}"
16
+ RUN_AZURE_TESTS="${INFRAPOOL_RUN_AZURE_TESTS:-${RUN_AZURE_TESTS:-false}}"
17
+ RUN_GCP_TESTS="${INFRAPOOL_RUN_GCP_TESTS:-${RUN_GCP_TESTS:-false}}"
18
+ export RUN_AWS_TESTS RUN_AZURE_TESTS RUN_GCP_TESTS
19
+
20
+ # authn-iam identifies the Conjur host by the AWS caller identity; the AWS stage
21
+ # derives these from `aws sts get-caller-identity` on the agent.
22
+ AWS_ACCOUNT_ID="${INFRAPOOL_AWS_ACCOUNT_ID:-${AWS_ACCOUNT_ID:-}}"
23
+ AWS_ROLE_NAME="${INFRAPOOL_AWS_ROLE_NAME:-${AWS_ROLE_NAME:-}}"
24
+ export AWS_ACCOUNT_ID AWS_ROLE_NAME
25
+
10
26
  source ./ci/oauth/keycloak/keycloak_functions.sh
11
27
  TOP_LEVEL=$(git rev-parse --show-toplevel)
12
28
 
@@ -23,6 +39,23 @@ if [ ! -f "${TOP_LEVEL}/VERSION" ]; then
23
39
  echo -n "0.0.dev" > "${TOP_LEVEL}/VERSION"
24
40
  fi
25
41
 
42
+ # Returns the cloud-authn cucumber tag to run when a RUN_*_TESTS flag is set,
43
+ # or empty for a normal run. Validates required env vars for the chosen cloud.
44
+ function cloudAuthnTag() {
45
+ if [[ "$RUN_AWS_TESTS" == "true" ]]; then
46
+ : "${AWS_ACCOUNT_ID:?AWS_ACCOUNT_ID is required for IAM tests (derived from aws sts get-caller-identity)}"
47
+ : "${AWS_ROLE_NAME:?AWS_ROLE_NAME is required for IAM tests (derived from aws sts get-caller-identity)}"
48
+ echo '@authn_iam'
49
+ elif [[ "$RUN_AZURE_TESTS" == "true" ]]; then
50
+ : "${AZURE_SUBSCRIPTION_ID:?AZURE_SUBSCRIPTION_ID is required for Azure tests}"
51
+ : "${AZURE_RESOURCE_GROUP:?AZURE_RESOURCE_GROUP is required for Azure tests}"
52
+ echo '@authn_azure'
53
+ elif [[ "$RUN_GCP_TESTS" == "true" ]]; then
54
+ : "${GCP_ID_TOKEN:?GCP_ID_TOKEN is required for GCP tests (run ci/get_gcp_token.sh first)}"
55
+ echo '@authn_gcp'
56
+ fi
57
+ }
58
+
26
59
  function main() {
27
60
  if ! docker info >/dev/null 2>&1; then
28
61
  echo "Docker does not seem to be running, run it first and retry"
@@ -31,6 +64,8 @@ function main() {
31
64
  # Generate reports folders locally
32
65
  mkdir -p spec/reports features/reports
33
66
 
67
+ CLOUD_AUTHN_TAG="$(cloudAuthnTag)"
68
+
34
69
  startConjur
35
70
  runTests
36
71
  }
@@ -54,6 +89,15 @@ function runTests() {
54
89
 
55
90
  local api_key=$(docker compose exec -T conjur conjurctl role retrieve-key cucumber:user:admin | tr -d '\r\n')
56
91
 
92
+ if [[ -n "$CLOUD_AUTHN_TAG" ]]; then
93
+ runCloudAuthnTests "$api_key"
94
+ else
95
+ runDefaultTests "$api_key"
96
+ fi
97
+ }
98
+
99
+ function runDefaultTests() {
100
+ local api_key="$1"
57
101
  echo 'Running tests'
58
102
  echo '-----'
59
103
  docker compose run --rm \
@@ -63,4 +107,38 @@ function runTests() {
63
107
  "/scripts/fetch_certificate && bundle exec rake jenkins_init jenkins_spec jenkins_cucumber"
64
108
  }
65
109
 
110
+ # Runs a single cloud-authenticator feature against real cloud infrastructure.
111
+ # Forwards the cloud provider env vars and clears proxies so the container can
112
+ # reach the link-local metadata endpoints (169.254.169.254, metadata.google.internal).
113
+ function runCloudAuthnTests() {
114
+ local api_key="$1"
115
+ echo "Running cloud authenticator tests: $CLOUD_AUTHN_TAG"
116
+ echo '-----'
117
+ local rc=0
118
+ docker compose run --rm \
119
+ -e CONJUR_AUTHN_API_KEY="$api_key" \
120
+ -e CLOUD_AUTHN_TAG="$CLOUD_AUTHN_TAG" \
121
+ -e NO_PROXY="169.254.169.254,metadata.google.internal,localhost,127.0.0.1,conjur" \
122
+ -e HTTP_PROXY="" -e HTTPS_PROXY="" -e http_proxy="" -e https_proxy="" \
123
+ -e AZURE_SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID:-}" \
124
+ -e AZURE_RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-}" \
125
+ -e USER_ASSIGNED_IDENTITY_CLIENT_ID="${USER_ASSIGNED_IDENTITY_CLIENT_ID:-}" \
126
+ -e GCP_ID_TOKEN="${GCP_ID_TOKEN:-}" \
127
+ -e GCP_PROJECT_ID="${GCP_PROJECT_ID:-}" \
128
+ -e AWS_ACCOUNT_ID="${AWS_ACCOUNT_ID:-}" \
129
+ -e AWS_ROLE_NAME="${AWS_ROLE_NAME:-}" \
130
+ tester \
131
+ "bundle exec rake jenkins_init jenkins_cucumber_cloud" || rc=$?
132
+
133
+ # On failure, dump the Conjur server log: authn-azure/iam/gcp rejections are
134
+ # logged there with the precise reason (issuer/annotation/audience mismatch),
135
+ # which the client only ever sees as an opaque 401.
136
+ if [[ $rc -ne 0 ]]; then
137
+ echo "=== cloud authn test failed; last 100 lines of Conjur server log ==="
138
+ docker compose logs --tail=100 conjur || true
139
+ echo '=== end Conjur server log ==='
140
+ fi
141
+ return $rc
142
+ }
143
+
66
144
  main
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: conjur-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.3.1.pre.846
4
+ version: 6.3.1.pre.848
5
5
  platform: ruby
6
6
  authors:
7
7
  - CyberArk Maintainers
@@ -261,6 +261,20 @@ dependencies:
261
261
  - - ">="
262
262
  - !ruby/object:Gem::Version
263
263
  version: '0'
264
+ - !ruby/object:Gem::Dependency
265
+ name: aws-sdk-core
266
+ requirement: !ruby/object:Gem::Requirement
267
+ requirements:
268
+ - - ">="
269
+ - !ruby/object:Gem::Version
270
+ version: '0'
271
+ type: :development
272
+ prerelease: false
273
+ version_requirements: !ruby/object:Gem::Requirement
274
+ requirements:
275
+ - - ">="
276
+ - !ruby/object:Gem::Version
277
+ version: '0'
264
278
  description: Conjur API
265
279
  email:
266
280
  - conj_maintainers@cyberark.com
@@ -291,6 +305,7 @@ files:
291
305
  - SECURITY.md
292
306
  - VERSION
293
307
  - ci/configure.sh
308
+ - ci/get_gcp_token.sh
294
309
  - ci/oauth/keycloak/create_client
295
310
  - ci/oauth/keycloak/create_user
296
311
  - ci/oauth/keycloak/fetch_certificate
@@ -305,8 +320,12 @@ files:
305
320
  - dev/stop
306
321
  - docker-compose.yml
307
322
  - example/demo.rb
323
+ - features/README.md
308
324
  - features/authenticators.feature
309
325
  - features/authn.feature
326
+ - features/authn_azure.feature
327
+ - features/authn_gcp.feature
328
+ - features/authn_iam.feature
310
329
  - features/authn_local.feature
311
330
  - features/exists.feature
312
331
  - features/group.feature
@@ -322,6 +341,7 @@ files:
322
341
  - features/role_fields.feature
323
342
  - features/rotate_api_key.feature
324
343
  - features/step_definitions/api_steps.rb
344
+ - features/step_definitions/cloud_authn_steps.rb
325
345
  - features/step_definitions/policy_steps.rb
326
346
  - features/step_definitions/result_steps.rb
327
347
  - features/support/env.rb
@@ -388,6 +408,7 @@ files:
388
408
  - lib/conjur/variable.rb
389
409
  - lib/conjur/webservice.rb
390
410
  - publish.sh
411
+ - secrets.yml
391
412
  - spec/.conjurrc
392
413
  - spec/api/host_factories_spec.rb
393
414
  - spec/api/policies_spec.rb
@@ -443,8 +464,12 @@ rubygems_version: 4.0.10
443
464
  specification_version: 4
444
465
  summary: Conjur API
445
466
  test_files:
467
+ - features/README.md
446
468
  - features/authenticators.feature
447
469
  - features/authn.feature
470
+ - features/authn_azure.feature
471
+ - features/authn_gcp.feature
472
+ - features/authn_iam.feature
448
473
  - features/authn_local.feature
449
474
  - features/exists.feature
450
475
  - features/group.feature
@@ -460,6 +485,7 @@ test_files:
460
485
  - features/role_fields.feature
461
486
  - features/rotate_api_key.feature
462
487
  - features/step_definitions/api_steps.rb
488
+ - features/step_definitions/cloud_authn_steps.rb
463
489
  - features/step_definitions/policy_steps.rb
464
490
  - features/step_definitions/result_steps.rb
465
491
  - features/support/env.rb