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

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: 52dbc5d9ceec78b99d6f3116a6b81731e0ef000c4acefec92f8748e62004817f
4
- data.tar.gz: 80c397da5014245c3783e29063427e848f634da126e5cd739876958f755ca612
3
+ metadata.gz: a22e04544db452dd276f8053e60ec40ba19dbeb6c8c74fa554f1abad9b9ddb37
4
+ data.tar.gz: 7997ace31344e73979de8a5e651526e0ca419862841e21475b5dec915ee5e444
5
5
  SHA512:
6
- metadata.gz: db2491c9bc1a5b40b71c8a9c762ca9df040b8d2054b2a683a0343d54a8c845e92dad42d7754ccce611f03d7692a623a61f6ebadbf9b5daf5aab76f4772666455
7
- data.tar.gz: 8e9b1daf341e59d9e8a97c562c399a1e30c8ad83c5981812c6df62bcc857a5e633ce938f43b9bb86ba790905965d56a9505a2b96077c56a52998f4d4b25456cb
6
+ metadata.gz: 36941d33ca17af4c45f9f475ae0dc25161a573bdb3c6d4a5f4c4d457078a1bc122fd63872d9983b835467a78e17ee0a0e0e6b5fd8186a95fd680459bf1f59e31
7
+ data.tar.gz: 0325c9377ed9aacd040f9d4cc0615c2ee0091460de22b52779cf4abec8fe9ed4e7d2ec6ec2ddde41fbecd9ad7099c2b1c3959776bcabd6275542afee8d592ba3
data/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## Unreleased
8
+ ### Added
9
+ - Cloud-native authenticators for Azure Managed Identity (`authn-azure`),
10
+ AWS IAM (`authn-iam`), and GCP workload identity (`authn-jwt/gcp`), exposed
11
+ via `Conjur::API.new_from_azure`, `.new_from_iam`, and `.new_from_gcp`
12
+ (CNJR-14213).
13
+
14
+ ### Changed
15
+ - CI uses Conjur Enterprise 1.27.0+, which removed the `GET /public_keys` endpoint
16
+ (CNJR-11339). Dropped the Cucumber scenario that exercised that API; the client
17
+ method remains for open-source Conjur.
18
+ - Retrieve the Cucumber admin API key via `conjurctl` in `test.sh` instead of rake,
19
+ avoiding Rails boot output corrupting the key on current Conjur images.
20
+ - CI now tests against Ruby 3.3, 3.4, and 4.0. Dropped Ruby 3.2 (end of life)
21
+ and raised the minimum supported Ruby version accordingly.
22
+
23
+ ### Fixed
24
+ - Set `CONJUR_AUTHN_ALLOWED_IP_RANGES` and `CONJUR_AUTHN_HTTP_ALLOWLIST` in the
25
+ test environment so Conjur's new SSRF guard does not block the authn-oidc
26
+ `provider-uri` fetch to the compose-local Keycloak server. (CNJR-13786)
8
27
 
9
28
  ## [6.3.1] - 2026-07-16
10
29
 
@@ -24,15 +43,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
24
43
  - Added `Conjur::API#fetch_policy` for retrieving policy data from the server,
25
44
  as YAML or JSON, with optional tree depth and size limits.
26
45
 
27
- ### Changed
28
- - CI uses Conjur Enterprise 1.27.0+, which removed the `GET /public_keys` endpoint
29
- (CNJR-11339). Dropped the Cucumber scenario that exercised that API; the client
30
- method remains for open-source Conjur.
31
- - Retrieve the Cucumber admin API key via `conjurctl` in `test.sh` instead of rake,
32
- avoiding Rails boot output corrupting the key on current Conjur images.
33
- - CI now tests against Ruby 3.3, 3.4, and 4.0. Dropped Ruby 3.2 (end of life)
34
- and raised the minimum supported Ruby version accordingly.
35
-
36
46
  ## [6.2.0] - 2026-06-05
37
47
 
38
48
  ## [6.1.0] - 2025-12-10
data/VERSION CHANGED
@@ -1 +1 @@
1
- 6.3.1-829
1
+ 6.3.1-846
data/docker-compose.yml CHANGED
@@ -11,6 +11,13 @@ services:
11
11
  environment:
12
12
  DATABASE_URL: postgres://postgres@pg/postgres
13
13
  CONJUR_DATA_KEY: 'WMfApcDBtocRWV+ZSUP3Tjr5XNU+Z2FdBb6BEezejIs='
14
+ # Conjur's SSRF guard blocks authenticator fetches (authn-oidc provider-uri here) that
15
+ # resolve to loopback/RFC1918 ranges or use plain HTTP. Keycloak is reached by compose
16
+ # service name, so it resolves inside the bridge network's private subnet and its
17
+ # discovery document advertises http://keycloak:8080 endpoints. Both guards must be
18
+ # opted out of for this test env. Mirrors conjur's own ci/docker-compose.yml.
19
+ CONJUR_AUTHN_ALLOWED_IP_RANGES: ${CONJUR_AUTHN_ALLOWED_IP_RANGES:-0.0.0.0/0,::/0}
20
+ CONJUR_AUTHN_HTTP_ALLOWLIST: ${CONJUR_AUTHN_HTTP_ALLOWLIST:-keycloak}
14
21
  volumes:
15
22
  - authn_local:/run/authn-local
16
23
  - ./ci/oauth/keycloak:/scripts
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+ require 'cgi'
7
+
8
+ module Conjur
9
+ class API
10
+ # Authenticator that uses an Azure Managed Identity JWT to obtain Conjur access tokens.
11
+ # Fetches a JWT from the Azure Instance Metadata Service (IMDS), then POSTs it
12
+ # to the Conjur authn-azure endpoint.
13
+ class AzureAuthenticator
14
+ include TokenExpiration
15
+
16
+ IMDS_TOKEN_PATH = '/metadata/identity/oauth2/token'.freeze
17
+ DEFAULT_RESOURCE = 'https://management.azure.com/'.freeze
18
+ DEFAULT_IMDS_BASE = 'http://169.254.169.254'.freeze
19
+ DEFAULT_IMDS_VER = '2018-02-01'.freeze
20
+
21
+ attr_reader :account, :service_id, :identity, :resource_uri,
22
+ :client_id, :imds_base_url, :imds_api_version
23
+
24
+ def initialize(account, service_id, identity,
25
+ resource_uri: DEFAULT_RESOURCE,
26
+ client_id: nil,
27
+ imds_base_url: DEFAULT_IMDS_BASE,
28
+ imds_api_version: DEFAULT_IMDS_VER)
29
+ @account = account
30
+ @service_id = service_id
31
+ @identity = identity
32
+ @resource_uri = resource_uri
33
+ @client_id = client_id
34
+ @imds_base_url = imds_base_url
35
+ @imds_api_version = imds_api_version
36
+ update_token_born
37
+ end
38
+
39
+ def refresh_token
40
+ Conjur::API.authenticate_azure(service_id, identity,
41
+ account: account,
42
+ resource_uri: resource_uri,
43
+ client_id: client_id,
44
+ imds_base_url: imds_base_url,
45
+ imds_api_version: imds_api_version).tap do
46
+ update_token_born
47
+ end
48
+ end
49
+ end
50
+
51
+ class << self
52
+ # Create a {Conjur::API} instance authenticated via Azure Managed Identity (authn-azure).
53
+ #
54
+ # @param [String] service_id the authn-azure service ID configured in Conjur
55
+ # @param [String] identity Conjur host identity (e.g. "host/my-app/azure-host")
56
+ # @param [String] account the Conjur organization account
57
+ # @param [String] resource_uri Azure resource URI for the IMDS token request
58
+ # @param [String, nil] client_id optional client ID for a user-assigned managed identity
59
+ # @param [String] imds_base_url override for the IMDS base URL (useful in tests)
60
+ # @param [String] imds_api_version override for the IMDS API version
61
+ # @param [String, nil] remote_ip optional IP address recorded in the audit log
62
+ # @return [Conjur::API]
63
+ def new_from_azure(service_id, identity,
64
+ account: Conjur.configuration.account,
65
+ resource_uri: AzureAuthenticator::DEFAULT_RESOURCE,
66
+ client_id: nil,
67
+ imds_base_url: AzureAuthenticator::DEFAULT_IMDS_BASE,
68
+ imds_api_version: AzureAuthenticator::DEFAULT_IMDS_VER,
69
+ remote_ip: nil)
70
+ self.new.init_from_azure(service_id, identity,
71
+ account: account,
72
+ resource_uri: resource_uri,
73
+ client_id: client_id,
74
+ imds_base_url: imds_base_url,
75
+ imds_api_version: imds_api_version,
76
+ remote_ip: remote_ip)
77
+ end
78
+
79
+ # Authenticate via authn-azure and return a parsed Conjur access token.
80
+ #
81
+ # @param [String] service_id the authn-azure service ID
82
+ # @param [String] identity Conjur host identity
83
+ # @param [String] account the Conjur organization account
84
+ # @return [Hash] parsed access token
85
+ def authenticate_azure(service_id, identity,
86
+ account: Conjur.configuration.account,
87
+ resource_uri: AzureAuthenticator::DEFAULT_RESOURCE,
88
+ client_id: nil,
89
+ imds_base_url: AzureAuthenticator::DEFAULT_IMDS_BASE,
90
+ imds_api_version: AzureAuthenticator::DEFAULT_IMDS_VER)
91
+ raise ArgumentError, "service_id is required" if service_id.nil? || service_id.empty?
92
+ raise ArgumentError, "identity is required" if identity.nil? || identity.empty?
93
+
94
+ if Conjur.log
95
+ Conjur.log << "Authenticating #{identity} to account #{account} via authn-azure/#{service_id}\n"
96
+ end
97
+
98
+ jwt = fetch_azure_jwt(resource_uri: resource_uri, client_id: client_id,
99
+ imds_base_url: imds_base_url, imds_api_version: imds_api_version)
100
+ JSON.parse(url_for(:authn_azure_authenticate, account, service_id, identity)
101
+ .post("jwt=#{CGI.escape(jwt)}", content_type: 'application/x-www-form-urlencoded'))
102
+ end
103
+
104
+ private
105
+
106
+ # Fetches an Azure Managed Identity JWT from IMDS.
107
+ # Uses a direct Net::HTTP connection (proxy addr = nil) because the IMDS
108
+ # link-local address 169.254.169.254 must never be routed through a proxy.
109
+ def fetch_azure_jwt(resource_uri:, client_id:, imds_base_url:, imds_api_version:)
110
+ query = "api-version=#{CGI.escape(imds_api_version)}&resource=#{CGI.escape(resource_uri)}"
111
+ query += "&client_id=#{CGI.escape(client_id)}" if client_id
112
+
113
+ uri = URI("#{imds_base_url}#{AzureAuthenticator::IMDS_TOKEN_PATH}?#{query}")
114
+ http = Net::HTTP.new(uri.host, uri.port, nil) # nil = no proxy
115
+ response = http.get(uri.request_uri, 'Metadata' => 'true')
116
+
117
+ unless response.is_a?(Net::HTTPSuccess)
118
+ raise "Azure IMDS token request failed: #{response.code} #{response.message}. Body: #{response.body}"
119
+ end
120
+
121
+ body = JSON.parse(response.body)
122
+ token = body['access_token']
123
+ raise "Azure IMDS returned an empty access token" if token.nil? || token.empty?
124
+
125
+ token
126
+ end
127
+ end
128
+
129
+ def init_from_azure(service_id, identity,
130
+ account: Conjur.configuration.account,
131
+ resource_uri: AzureAuthenticator::DEFAULT_RESOURCE,
132
+ client_id: nil,
133
+ imds_base_url: AzureAuthenticator::DEFAULT_IMDS_BASE,
134
+ imds_api_version: AzureAuthenticator::DEFAULT_IMDS_VER,
135
+ remote_ip: nil)
136
+ @remote_ip = remote_ip
137
+ @authenticator = AzureAuthenticator.new(account, service_id, identity,
138
+ resource_uri: resource_uri,
139
+ client_id: client_id,
140
+ imds_base_url: imds_base_url,
141
+ imds_api_version: imds_api_version)
142
+ self
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'cgi'
6
+ require 'json'
7
+
8
+ module Conjur
9
+ class API
10
+ # Authenticator that uses a GCP identity token to obtain Conjur access tokens.
11
+ # Fetches an identity JWT from the GCP Instance Metadata Service, then POSTs it
12
+ # to the Conjur authn-jwt/gcp endpoint.
13
+ #
14
+ # An explicit JWT may be supplied to bypass the metadata service fetch —
15
+ # useful in CI environments where the token is pre-fetched on a GCP agent.
16
+ class GCPAuthenticator
17
+ include TokenExpiration
18
+
19
+ DEFAULT_SERVICE_ID = 'gcp'.freeze
20
+ DEFAULT_METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1' \
21
+ '/instance/service-accounts/default/identity'.freeze
22
+
23
+ attr_reader :account, :service_id, :identity, :jwt, :gcp_identity_url, :audience
24
+
25
+ def initialize(account, service_id, identity, jwt: nil,
26
+ gcp_identity_url: DEFAULT_METADATA_URL, audience: nil)
27
+ @account = account
28
+ @service_id = service_id
29
+ @identity = identity
30
+ @jwt = jwt
31
+ @gcp_identity_url = gcp_identity_url
32
+ @audience = audience
33
+ update_token_born
34
+ end
35
+
36
+ def refresh_token
37
+ Conjur::API.authenticate_gcp(identity, account: account,
38
+ service_id: service_id,
39
+ jwt: jwt,
40
+ gcp_identity_url: gcp_identity_url,
41
+ audience: audience).tap do
42
+ update_token_born
43
+ end
44
+ end
45
+ end
46
+
47
+ class << self
48
+ # Create a {Conjur::API} instance authenticated via GCP workload identity (authn-jwt/gcp).
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")
52
+ # @param [String] account the Conjur organization account
53
+ # @param [String, nil] jwt pre-obtained GCP identity token; when supplied the metadata
54
+ # service is not called. Useful in tests and CI where the token is pre-fetched.
55
+ # @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
+ # @param [String, nil] remote_ip optional IP address recorded in the audit log
60
+ # @return [Conjur::API]
61
+ def new_from_gcp(identity,
62
+ account: Conjur.configuration.account,
63
+ service_id: GCPAuthenticator::DEFAULT_SERVICE_ID,
64
+ jwt: nil,
65
+ gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL,
66
+ audience: nil,
67
+ 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)
71
+ end
72
+
73
+ # Authenticate via authn-jwt/gcp and return a parsed Conjur access token.
74
+ #
75
+ # @param [String] identity Conjur host identity
76
+ # @param [String] service_id the authn-jwt service ID
77
+ # @param [String] account the Conjur organization account
78
+ # @param [String, nil] jwt pre-obtained GCP identity token
79
+ # @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
+ # @return [Hash] parsed access token
84
+ def authenticate_gcp(identity,
85
+ account: Conjur.configuration.account,
86
+ service_id: GCPAuthenticator::DEFAULT_SERVICE_ID,
87
+ 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?
92
+
93
+ if Conjur.log
94
+ Conjur.log << "Authenticating #{identity} to account #{account} via authn-jwt/#{service_id}\n"
95
+ end
96
+
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)
100
+ .post("jwt=#{CGI.escape(token)}", content_type: 'application/x-www-form-urlencoded'))
101
+ end
102
+
103
+ private
104
+
105
+ # Fetches a GCP identity token from the instance metadata service.
106
+ # Uses a direct Net::HTTP connection (proxy addr = nil) because the metadata
107
+ # link-local address must never be routed through a proxy.
108
+ #
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)
114
+ url = "#{gcp_identity_url}?#{URI.encode_www_form(audience: aud, format: 'full')}"
115
+
116
+ uri = URI(url)
117
+ http = Net::HTTP.new(uri.host, uri.port, nil) # nil = no proxy
118
+ response = http.get(uri.request_uri, 'Metadata-Flavor' => 'Google')
119
+
120
+ unless response.is_a?(Net::HTTPSuccess)
121
+ raise "GCP metadata token request failed: #{response.code} #{response.message}. Body: #{response.body}"
122
+ end
123
+
124
+ token = response.body&.strip
125
+ raise "GCP metadata service returned an empty token" if token.nil? || token.empty?
126
+
127
+ token
128
+ end
129
+
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
132
+ # supplied identity and is normalized away before being re-applied.
133
+ def default_gcp_audience(account, identity)
134
+ normalized = identity.start_with?('host/') ? identity['host/'.length..] : identity
135
+ "conjur/#{account}/host/#{normalized}"
136
+ end
137
+ end
138
+
139
+ def init_from_gcp(identity,
140
+ account: Conjur.configuration.account,
141
+ service_id: GCPAuthenticator::DEFAULT_SERVICE_ID,
142
+ jwt: nil,
143
+ gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL,
144
+ audience: nil,
145
+ remote_ip: nil)
146
+ @remote_ip = remote_ip
147
+ @authenticator = GCPAuthenticator.new(account, service_id, identity,
148
+ jwt: jwt, gcp_identity_url: gcp_identity_url,
149
+ audience: audience)
150
+ self
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'uri'
5
+
6
+ module Conjur
7
+ class API
8
+ # Authenticator that uses AWS IAM credentials to obtain Conjur access tokens.
9
+ # Signs an AWS STS GetCallerIdentity request with SigV4, then POSTs the
10
+ # resulting headers as JSON to the Conjur authn-iam endpoint.
11
+ #
12
+ # Requires the +aws-sdk-core+ gem (not bundled with conjur-api).
13
+ # Add `gem 'aws-sdk-core'` to your Gemfile to use this authenticator.
14
+ class IAMAuthenticator
15
+ include TokenExpiration
16
+
17
+ DEFAULT_SERVICE_ID = 'prod'.freeze
18
+ DEFAULT_REGION = 'us-east-1'.freeze
19
+
20
+ STS_GLOBAL_URL = 'https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15'.freeze
21
+ STS_REGIONAL_URL = 'https://sts.%<region>s.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15'.freeze
22
+ STS_GLOBAL_SIGNING_REGION = 'us-east-1'.freeze
23
+
24
+ attr_reader :account, :service_id, :identity, :aws_region
25
+
26
+ def initialize(account, service_id, identity, aws_region = DEFAULT_REGION)
27
+ @account = account
28
+ @service_id = service_id
29
+ @identity = identity
30
+ @aws_region = aws_region
31
+ update_token_born
32
+ end
33
+
34
+ def refresh_token
35
+ Conjur::API.authenticate_iam(identity, account: account,
36
+ service_id: service_id,
37
+ aws_region: aws_region).tap do
38
+ update_token_born
39
+ end
40
+ end
41
+ end
42
+
43
+ class << self
44
+ # Create a {Conjur::API} instance authenticated via AWS IAM (authn-iam).
45
+ #
46
+ # AWS credentials are resolved from the default chain: environment variables
47
+ # (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN), then
48
+ # ~/.aws/credentials, then the EC2 instance profile.
49
+ #
50
+ # Requires the +aws-sdk-core+ gem.
51
+ #
52
+ # @param [String] identity Conjur host identity (e.g. "host/my-app/my-role")
53
+ # @param [String] service_id the authn-iam service ID (default: "prod")
54
+ # @param [String] account the Conjur organization account
55
+ # @param [String] aws_region AWS region for STS signing. Use "global" for the
56
+ # global STS endpoint (signed as us-east-1).
57
+ # @param [String, nil] remote_ip optional IP address recorded in the audit log
58
+ # @return [Conjur::API]
59
+ def new_from_iam(identity,
60
+ account: Conjur.configuration.account,
61
+ service_id: IAMAuthenticator::DEFAULT_SERVICE_ID,
62
+ aws_region: IAMAuthenticator::DEFAULT_REGION,
63
+ remote_ip: nil)
64
+ self.new.init_from_iam(identity, account: account, service_id: service_id,
65
+ aws_region: aws_region, remote_ip: remote_ip)
66
+ end
67
+
68
+ # Authenticate via authn-iam and return a parsed Conjur access token.
69
+ #
70
+ # @param [String] identity Conjur host identity
71
+ # @param [String] service_id the authn-iam service ID
72
+ # @param [String] account the Conjur organization account
73
+ # @param [String] aws_region AWS region for STS signing
74
+ # @return [Hash] parsed access token
75
+ def authenticate_iam(identity,
76
+ account: Conjur.configuration.account,
77
+ service_id: IAMAuthenticator::DEFAULT_SERVICE_ID,
78
+ aws_region: IAMAuthenticator::DEFAULT_REGION)
79
+ raise ArgumentError, "identity is required" if identity.nil? || identity.empty?
80
+ raise ArgumentError, "service_id is required" if service_id.nil? || service_id.empty?
81
+
82
+ begin
83
+ require 'aws-sdk-core'
84
+ rescue LoadError
85
+ raise LoadError,
86
+ "aws-sdk-core is required for IAM authentication. " \
87
+ "Add `gem 'aws-sdk-core'` to your Gemfile."
88
+ end
89
+
90
+ if Conjur.log
91
+ Conjur.log << "Authenticating #{identity} to account #{account} via authn-iam/#{service_id}\n"
92
+ end
93
+
94
+ signed_headers = build_iam_signed_headers(aws_region)
95
+ JSON.parse(url_for(:authn_iam_authenticate, account, service_id, identity)
96
+ .post(signed_headers.to_json, content_type: 'application/json'))
97
+ end
98
+
99
+ private
100
+
101
+ def build_iam_signed_headers(aws_region)
102
+ signing_region = aws_region == 'global' ? IAMAuthenticator::STS_GLOBAL_SIGNING_REGION : aws_region
103
+ sts_url = aws_region == 'global' || aws_region == IAMAuthenticator::STS_GLOBAL_SIGNING_REGION \
104
+ ? IAMAuthenticator::STS_GLOBAL_URL
105
+ : format(IAMAuthenticator::STS_REGIONAL_URL, region: aws_region)
106
+
107
+ provider = Aws::CredentialProviderChain.new.resolve
108
+ raise ArgumentError,
109
+ "No AWS credentials found. Configure an instance profile, " \
110
+ "environment variables (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY), " \
111
+ "or ~/.aws/credentials." unless provider
112
+
113
+ creds = provider.credentials
114
+ signer = Aws::Sigv4::Signer.new(
115
+ service: 'sts',
116
+ region: signing_region,
117
+ access_key_id: creds.access_key_id,
118
+ secret_access_key: creds.secret_access_key,
119
+ session_token: creds.session_token
120
+ )
121
+
122
+ signed = signer.sign_request(http_method: 'GET', url: sts_url, headers: {}, body: '')
123
+ headers = signed.headers.dup
124
+ # Conjur expects these specific keys
125
+ headers['host'] = URI.parse(sts_url).host
126
+ headers.compact
127
+ end
128
+ end
129
+
130
+ def init_from_iam(identity,
131
+ account: Conjur.configuration.account,
132
+ service_id: IAMAuthenticator::DEFAULT_SERVICE_ID,
133
+ aws_region: IAMAuthenticator::DEFAULT_REGION,
134
+ remote_ip: nil)
135
+ @remote_ip = remote_ip
136
+ @authenticator = IAMAuthenticator.new(account, service_id, identity, aws_region)
137
+ self
138
+ end
139
+ end
140
+ end
@@ -42,6 +42,30 @@ module Conjur
42
42
  )[fully_escape account][fully_escape username]['authenticate']
43
43
  end
44
44
 
45
+ # POST /authn-azure/{service_id}/{account}/{login}/authenticate
46
+ def authn_azure_authenticate account, service_id, identity
47
+ RestClient::Resource.new(
48
+ Conjur.configuration.core_url,
49
+ Conjur.configuration.rest_client_options
50
+ )['authn-azure'][fully_escape service_id][fully_escape account][fully_escape identity]['authenticate']
51
+ end
52
+
53
+ # POST /authn-iam/{service_id}/{account}/{login}/authenticate
54
+ def authn_iam_authenticate account, service_id, identity
55
+ RestClient::Resource.new(
56
+ Conjur.configuration.core_url,
57
+ Conjur.configuration.rest_client_options
58
+ )['authn-iam'][fully_escape service_id][fully_escape account][fully_escape identity]['authenticate']
59
+ end
60
+
61
+ # POST /authn-jwt/{service_id}/{account}/{login}/authenticate
62
+ def authn_gcp_authenticate account, service_id, identity
63
+ RestClient::Resource.new(
64
+ Conjur.configuration.core_url,
65
+ Conjur.configuration.rest_client_options
66
+ )['authn-jwt'][fully_escape service_id][fully_escape account][fully_escape identity]['authenticate']
67
+ end
68
+
45
69
  # Builds the RestClient::Resource for an authn-cert authentication request.
46
70
  # cert_options must include :ssl_client_cert and :ssl_client_key so that
47
71
  # the client certificate is presented during the TLS handshake.
data/lib/conjur/api.rb CHANGED
@@ -36,6 +36,9 @@ require 'conjur/log_source'
36
36
  require 'conjur/has_attributes'
37
37
  require 'conjur/api/authenticators'
38
38
  require 'conjur/api/authn'
39
+ require 'conjur/api/authn_azure'
40
+ require 'conjur/api/authn_iam'
41
+ require 'conjur/api/authn_gcp'
39
42
  require 'conjur/api/authn_cert'
40
43
  require 'conjur/api/roles'
41
44
  require 'conjur/api/resources'
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'cgi'
5
+ require 'conjur/api/router'
6
+
7
+ describe Conjur::API, api: :dummy do
8
+ let(:service_id) { 'my-azure-service' }
9
+ let(:identity) { 'host/my-app/azure-host' }
10
+ let(:azure_jwt) { 'eyJhbGciOiJSUzI1NiJ9.azure-payload.sig' }
11
+ let(:raw_token) { { 'protected' => 'p', 'payload' => 'pl', 'signature' => 's' } }
12
+
13
+ describe '.new_from_azure' do
14
+ it 'returns an API instance with an AzureAuthenticator' do
15
+ allow(Conjur::API).to receive(:authenticate_azure).and_return(raw_token)
16
+ api_instance = Conjur::API.new_from_azure(service_id, identity, account: account)
17
+ expect(api_instance).to be_a(Conjur::API)
18
+ expect(api_instance.authenticator).to be_a(Conjur::API::AzureAuthenticator)
19
+ end
20
+ end
21
+
22
+ describe '.authenticate_azure' do
23
+ let(:resource) { double('resource') }
24
+
25
+ before do
26
+ allow(Conjur::API).to receive(:url_for)
27
+ .with(:authn_azure_authenticate, account, service_id, identity)
28
+ .and_return(resource)
29
+ allow(resource).to receive(:post)
30
+ .with("jwt=#{CGI.escape(azure_jwt)}", content_type: 'application/x-www-form-urlencoded')
31
+ .and_return(raw_token.to_json)
32
+ allow(Conjur::API).to receive(:fetch_azure_jwt).and_return(azure_jwt)
33
+ end
34
+
35
+ it 'returns a parsed token' do
36
+ token = Conjur::API.authenticate_azure(service_id, identity, account: account)
37
+ expect(token).to eq(raw_token)
38
+ end
39
+
40
+ it 'raises ArgumentError when service_id is empty' do
41
+ expect { Conjur::API.authenticate_azure('', identity, account: account) }
42
+ .to raise_error(ArgumentError, /service_id is required/)
43
+ end
44
+
45
+ it 'raises ArgumentError when identity is empty' do
46
+ expect { Conjur::API.authenticate_azure(service_id, '', account: account) }
47
+ .to raise_error(ArgumentError, /identity is required/)
48
+ end
49
+ end
50
+
51
+ describe 'AzureAuthenticator#refresh_token' do
52
+ subject(:authenticator) do
53
+ Conjur::API::AzureAuthenticator.new(account, service_id, identity)
54
+ end
55
+
56
+ it 'delegates to authenticate_azure with stored credentials' do
57
+ expect(Conjur::API).to receive(:authenticate_azure)
58
+ .with(service_id, identity,
59
+ account: account,
60
+ resource_uri: Conjur::API::AzureAuthenticator::DEFAULT_RESOURCE,
61
+ client_id: nil,
62
+ imds_base_url: Conjur::API::AzureAuthenticator::DEFAULT_IMDS_BASE,
63
+ imds_api_version: Conjur::API::AzureAuthenticator::DEFAULT_IMDS_VER)
64
+ .and_return(raw_token)
65
+ expect(authenticator.refresh_token).to eq(raw_token)
66
+ end
67
+ end
68
+ end
69
+
70
+ describe Conjur::API::Router do
71
+ let(:account) { 'myaccount' }
72
+ let(:service_id) { 'my-service' }
73
+ let(:identity) { 'host/my-app/azure-host' }
74
+
75
+ before do
76
+ allow(Conjur.configuration).to receive(:core_url).and_return('https://conjur.example.com')
77
+ allow(Conjur.configuration).to receive(:rest_client_options).and_return({})
78
+ end
79
+
80
+ describe '#authn_azure_authenticate' do
81
+ subject { described_class.authn_azure_authenticate(account, service_id, identity) }
82
+
83
+ it 'returns a RestClient::Resource' do
84
+ expect(subject).to be_a(RestClient::Resource)
85
+ end
86
+
87
+ it 'builds the correct URL' do
88
+ expect(subject.url).to end_with('/authn-azure/my-service/myaccount/host%2Fmy-app%2Fazure-host/authenticate')
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'cgi'
5
+ require 'conjur/api/router'
6
+
7
+ describe Conjur::API, api: :dummy do
8
+ let(:service_id) { 'gcp' }
9
+ let(:identity) { 'host/data/test/gcp-apps/test-app' }
10
+ let(:gcp_jwt) { 'eyJhbGciOiJSUzI1NiJ9.gcp-payload.sig' }
11
+ let(:raw_token) { { 'protected' => 'p', 'payload' => 'pl', 'signature' => 's' } }
12
+
13
+ describe '.new_from_gcp' do
14
+ it 'returns an API instance with a GCPAuthenticator' do
15
+ allow(Conjur::API).to receive(:authenticate_gcp).and_return(raw_token)
16
+ api_instance = Conjur::API.new_from_gcp(identity, account: account)
17
+ expect(api_instance).to be_a(Conjur::API)
18
+ expect(api_instance.authenticator).to be_a(Conjur::API::GCPAuthenticator)
19
+ end
20
+ end
21
+
22
+ describe '.authenticate_gcp' do
23
+ let(:resource) { double('resource') }
24
+
25
+ before do
26
+ allow(Conjur::API).to receive(:url_for)
27
+ .with(:authn_gcp_authenticate, account, service_id, identity)
28
+ .and_return(resource)
29
+ allow(resource).to receive(:post)
30
+ .with("jwt=#{CGI.escape(gcp_jwt)}", content_type: 'application/x-www-form-urlencoded')
31
+ .and_return(raw_token.to_json)
32
+ end
33
+
34
+ context 'with a pre-supplied JWT' do
35
+ it 'returns a parsed token without calling the metadata service' do
36
+ 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)
40
+ expect(token).to eq(raw_token)
41
+ end
42
+ end
43
+
44
+ context 'without a pre-supplied JWT' do
45
+ it 'fetches from the metadata service' do
46
+ 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)
48
+ expect(token).to eq(raw_token)
49
+ end
50
+ end
51
+
52
+ it 'raises ArgumentError when identity is empty' do
53
+ expect { Conjur::API.authenticate_gcp('', account: account) }
54
+ .to raise_error(ArgumentError, /identity is required/)
55
+ 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
+ end
62
+
63
+ describe 'GCPAuthenticator#refresh_token' do
64
+ context 'with a pre-supplied JWT' do
65
+ subject(:authenticator) do
66
+ Conjur::API::GCPAuthenticator.new(account, service_id, identity, jwt: gcp_jwt)
67
+ end
68
+
69
+ it 'passes the stored JWT to authenticate_gcp' do
70
+ expect(Conjur::API).to receive(:authenticate_gcp)
71
+ .with(identity,
72
+ account: account,
73
+ service_id: service_id,
74
+ jwt: gcp_jwt,
75
+ gcp_identity_url: Conjur::API::GCPAuthenticator::DEFAULT_METADATA_URL,
76
+ audience: nil)
77
+ .and_return(raw_token)
78
+ expect(authenticator.refresh_token).to eq(raw_token)
79
+ end
80
+ end
81
+
82
+ context 'without a pre-supplied JWT' do
83
+ subject(:authenticator) do
84
+ Conjur::API::GCPAuthenticator.new(account, service_id, identity)
85
+ end
86
+
87
+ it 'passes nil jwt to authenticate_gcp (which then calls metadata service)' do
88
+ expect(Conjur::API).to receive(:authenticate_gcp)
89
+ .with(identity,
90
+ 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
+ jwt: nil,
112
+ gcp_identity_url: Conjur::API::GCPAuthenticator::DEFAULT_METADATA_URL,
113
+ audience: 'my-custom-audience')
114
+ .and_return(raw_token)
115
+ expect(authenticator.refresh_token).to eq(raw_token)
116
+ end
117
+ end
118
+ end
119
+
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
143
+ it 'strips a leading host/ and rebuilds the conjur audience' do
144
+ expect(Conjur::API.send(:default_gcp_audience, account, 'host/data/app'))
145
+ .to eq("conjur/#{account}/host/data/app")
146
+ end
147
+
148
+ it 'assumes a host identity when the host/ prefix is absent' do
149
+ expect(Conjur::API.send(:default_gcp_audience, account, 'data/app'))
150
+ .to eq("conjur/#{account}/host/data/app")
151
+ end
152
+ end
153
+ end
154
+
155
+ describe Conjur::API::Router do
156
+ let(:account) { 'myaccount' }
157
+ let(:service_id) { 'gcp' }
158
+ let(:identity) { 'host/data/test/gcp-apps/test-app' }
159
+
160
+ before do
161
+ allow(Conjur.configuration).to receive(:core_url).and_return('https://conjur.example.com')
162
+ allow(Conjur.configuration).to receive(:rest_client_options).and_return({})
163
+ end
164
+
165
+ describe '#authn_gcp_authenticate' do
166
+ subject { described_class.authn_gcp_authenticate(account, service_id, identity) }
167
+
168
+ it 'returns a RestClient::Resource' do
169
+ expect(subject).to be_a(RestClient::Resource)
170
+ end
171
+
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')
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'conjur/api/router'
5
+
6
+ describe Conjur::API, api: :dummy do
7
+ let(:service_id) { 'prod' }
8
+ let(:identity) { 'host/my-app/my-role' }
9
+ let(:raw_token) { { 'protected' => 'p', 'payload' => 'pl', 'signature' => 's' } }
10
+ let(:signed_headers) do
11
+ {
12
+ 'Authorization' => 'AWS4-HMAC-SHA256 Credential=AKIAEXAMPLE/...',
13
+ 'x-amz-date' => '20240101T000000Z',
14
+ 'x-amz-content-sha256' => 'e3b0c44298fc1c149afbf4c8996fb924',
15
+ 'x-amz-security-token' => 'token',
16
+ 'host' => 'sts.amazonaws.com'
17
+ }
18
+ end
19
+
20
+ describe '.new_from_iam' do
21
+ it 'returns an API instance with an IAMAuthenticator' do
22
+ allow(Conjur::API).to receive(:authenticate_iam).and_return(raw_token)
23
+ api_instance = Conjur::API.new_from_iam(identity, account: account)
24
+ expect(api_instance).to be_a(Conjur::API)
25
+ expect(api_instance.authenticator).to be_a(Conjur::API::IAMAuthenticator)
26
+ end
27
+ end
28
+
29
+ describe '.authenticate_iam' do
30
+ let(:resource) { double('resource') }
31
+
32
+ before do
33
+ allow(Conjur::API).to receive(:require).with('aws-sdk-core')
34
+ allow(Conjur::API).to receive(:url_for)
35
+ .with(:authn_iam_authenticate, account, service_id, identity)
36
+ .and_return(resource)
37
+ allow(resource).to receive(:post)
38
+ .with(signed_headers.to_json, content_type: 'application/json')
39
+ .and_return(raw_token.to_json)
40
+ allow(Conjur::API).to receive(:build_iam_signed_headers).and_return(signed_headers)
41
+ end
42
+
43
+ it 'returns a parsed token' do
44
+ token = Conjur::API.authenticate_iam(identity, account: account, service_id: service_id)
45
+ expect(token).to eq(raw_token)
46
+ end
47
+
48
+ it 'raises ArgumentError when identity is empty' do
49
+ expect { Conjur::API.authenticate_iam('', account: account) }
50
+ .to raise_error(ArgumentError, /identity is required/)
51
+ end
52
+
53
+ it 'raises ArgumentError when service_id is empty' do
54
+ expect { Conjur::API.authenticate_iam(identity, account: account, service_id: '') }
55
+ .to raise_error(ArgumentError, /service_id is required/)
56
+ end
57
+
58
+ it 'raises LoadError when aws-sdk-core is unavailable' do
59
+ allow(Conjur::API).to receive(:require).with('aws-sdk-core').and_raise(LoadError)
60
+ expect { Conjur::API.authenticate_iam(identity, account: account) }
61
+ .to raise_error(LoadError, /aws-sdk-core/)
62
+ end
63
+ end
64
+
65
+ describe 'IAMAuthenticator#refresh_token' do
66
+ subject(:authenticator) do
67
+ Conjur::API::IAMAuthenticator.new(account, service_id, identity)
68
+ end
69
+
70
+ it 'delegates to authenticate_iam with stored credentials' do
71
+ expect(Conjur::API).to receive(:authenticate_iam)
72
+ .with(identity,
73
+ account: account,
74
+ service_id: service_id,
75
+ aws_region: Conjur::API::IAMAuthenticator::DEFAULT_REGION)
76
+ .and_return(raw_token)
77
+ expect(authenticator.refresh_token).to eq(raw_token)
78
+ end
79
+ end
80
+ end
81
+
82
+ describe Conjur::API::Router do
83
+ let(:account) { 'myaccount' }
84
+ let(:service_id) { 'prod' }
85
+ let(:identity) { 'host/my-app/my-role' }
86
+
87
+ before do
88
+ allow(Conjur.configuration).to receive(:core_url).and_return('https://conjur.example.com')
89
+ allow(Conjur.configuration).to receive(:rest_client_options).and_return({})
90
+ end
91
+
92
+ describe '#authn_iam_authenticate' do
93
+ subject { described_class.authn_iam_authenticate(account, service_id, identity) }
94
+
95
+ it 'returns a RestClient::Resource' do
96
+ expect(subject).to be_a(RestClient::Resource)
97
+ end
98
+
99
+ it 'builds the correct URL' do
100
+ expect(subject.url).to end_with('/authn-iam/prod/myaccount/host%2Fmy-app%2Fmy-role/authenticate')
101
+ end
102
+ end
103
+ end
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.829
4
+ version: 6.3.1.pre.846
5
5
  platform: ruby
6
6
  authors:
7
7
  - CyberArk Maintainers
@@ -342,7 +342,10 @@ files:
342
342
  - lib/conjur/api.rb
343
343
  - lib/conjur/api/authenticators.rb
344
344
  - lib/conjur/api/authn.rb
345
+ - lib/conjur/api/authn_azure.rb
345
346
  - lib/conjur/api/authn_cert.rb
347
+ - lib/conjur/api/authn_gcp.rb
348
+ - lib/conjur/api/authn_iam.rb
346
349
  - lib/conjur/api/host_factories.rb
347
350
  - lib/conjur/api/ldap_sync.rb
348
351
  - lib/conjur/api/policies.rb
@@ -392,7 +395,10 @@ files:
392
395
  - spec/api/server_version_spec.rb
393
396
  - spec/api/version_check_spec.rb
394
397
  - spec/api_spec.rb
398
+ - spec/authn_azure_spec.rb
395
399
  - spec/authn_cert_spec.rb
400
+ - spec/authn_gcp_spec.rb
401
+ - spec/authn_iam_spec.rb
396
402
  - spec/base_object_spec.rb
397
403
  - spec/cert_utils_spec.rb
398
404
  - spec/cidr_spec.rb
@@ -471,7 +477,10 @@ test_files:
471
477
  - spec/api/server_version_spec.rb
472
478
  - spec/api/version_check_spec.rb
473
479
  - spec/api_spec.rb
480
+ - spec/authn_azure_spec.rb
474
481
  - spec/authn_cert_spec.rb
482
+ - spec/authn_gcp_spec.rb
483
+ - spec/authn_iam_spec.rb
475
484
  - spec/base_object_spec.rb
476
485
  - spec/cert_utils_spec.rb
477
486
  - spec/cidr_spec.rb