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 +4 -4
- data/CHANGELOG.md +19 -9
- data/VERSION +1 -1
- data/docker-compose.yml +7 -0
- data/lib/conjur/api/authn_azure.rb +145 -0
- data/lib/conjur/api/authn_gcp.rb +153 -0
- data/lib/conjur/api/authn_iam.rb +140 -0
- data/lib/conjur/api/router.rb +24 -0
- data/lib/conjur/api.rb +3 -0
- data/spec/authn_azure_spec.rb +91 -0
- data/spec/authn_gcp_spec.rb +176 -0
- data/spec/authn_iam_spec.rb +103 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a22e04544db452dd276f8053e60ec40ba19dbeb6c8c74fa554f1abad9b9ddb37
|
|
4
|
+
data.tar.gz: 7997ace31344e73979de8a5e651526e0ca419862841e21475b5dec915ee5e444
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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-
|
|
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
|
data/lib/conjur/api/router.rb
CHANGED
|
@@ -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.
|
|
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
|