Skip to content

Commit e7fa675

Browse files
committed
Support sensitive values in to_json_pretty
The idea is being able to do something like this, (where just the call to `to_json_pretty` is explicitly `Deferred` - needed because `node_encrypt` returns a `Deferred`) ```puppet file { '/etc/my_secret.json': content => Deferred('to_json_pretty',[{ username => 'myuser', password => lookup('my_eyaml_secret').node_encrypt::secret, }]), } ``` ... instead of having to also explicitly defer `unwrap` and `Sensitive` and end up with a huge mess similar to... ```puppet file { '/etc/my_secret.json': content => Sensitive( Deferred('to_json_pretty',[{ username => 'myuser', password => Deferred('unwrap', [lookup('my_eyaml_secret').node_encrypt::secret]), }]) ), } ``` The thought behind `rewrap_sensitive_data` is it makes it easy to extend this functionality into other similar functions, (`to_yaml`, `to_toml` etc.) Later, we might consider adding a `deferrable_to_XXX` functions to further simplify this sort of use-case.
1 parent 802eb9a commit e7fa675

File tree

5 files changed

+225
-3
lines changed

5 files changed

+225
-3
lines changed

Diff for: REFERENCE.md

+45
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ This function will return list of nested Hash values and returns list of values
135135
* [`stdlib::parsehocon`](#stdlib--parsehocon): This function accepts HOCON as a string and converts it into the correct
136136
Puppet structure
137137
* [`stdlib::powershell_escape`](#stdlib--powershell_escape): Escapes a string so that it can be safely used in a PowerShell command line.
138+
* [`stdlib::rewrap_sensitive_data`](#stdlib--rewrap_sensitive_data): Unwraps any sensitives in data and returns a sensitive
138139
* [`stdlib::seeded_rand`](#stdlib--seeded_rand): Generates a random whole number greater than or equal to 0 and less than max, using the value of seed for repeatable randomness.
139140
* [`stdlib::seeded_rand_string`](#stdlib--seeded_rand_string): Generates a consistent random string of specific length based on provided seed.
140141
* [`stdlib::sha256`](#stdlib--sha256): Run a SHA256 calculation against a given value.
@@ -3837,6 +3838,50 @@ Data type: `Any`
38373838

38383839
The string to escape
38393840

3841+
### <a name="stdlib--rewrap_sensitive_data"></a>`stdlib::rewrap_sensitive_data`
3842+
3843+
Type: Ruby 4.x API
3844+
3845+
It's not uncommon to have Sensitive strings as values within a hash or array.
3846+
Before passing the data to a type property or another function, it's useful
3847+
to be able to `unwrap` these values first. This function does this. If
3848+
sensitive data was included in the data, the whole result is then rewrapped
3849+
as Sensitive.
3850+
3851+
Optionally, this function can be passed a block. When a block is given, it will
3852+
be run with the unwrapped data, but before the final rewrapping. This is useful
3853+
to provide transparent rewrapping to other functions in stdlib especially.
3854+
3855+
This is analogous to the way `epp` transparently handles sensitive parameters.
3856+
3857+
#### `stdlib::rewrap_sensitive_data(Any $data, Optional[Callable[Any]] &$block)`
3858+
3859+
It's not uncommon to have Sensitive strings as values within a hash or array.
3860+
Before passing the data to a type property or another function, it's useful
3861+
to be able to `unwrap` these values first. This function does this. If
3862+
sensitive data was included in the data, the whole result is then rewrapped
3863+
as Sensitive.
3864+
3865+
Optionally, this function can be passed a block. When a block is given, it will
3866+
be run with the unwrapped data, but before the final rewrapping. This is useful
3867+
to provide transparent rewrapping to other functions in stdlib especially.
3868+
3869+
This is analogous to the way `epp` transparently handles sensitive parameters.
3870+
3871+
Returns: `Any` Returns the rewrapped data
3872+
3873+
##### `data`
3874+
3875+
Data type: `Any`
3876+
3877+
The data
3878+
3879+
##### `&block`
3880+
3881+
Data type: `Optional[Callable[Any]]`
3882+
3883+
A lambda that will be run after the data has been unwrapped, but before it is rewrapped, (if it contained sensitives)
3884+
38403885
### <a name="stdlib--seeded_rand"></a>`stdlib::seeded_rand`
38413886

38423887
Type: Ruby 4.x API

Diff for: lib/puppet/functions/stdlib/rewrap_sensitive_data.rb

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
# @summary Unwraps any sensitives in data and returns a sensitive
4+
#
5+
# It's not uncommon to have Sensitive strings as values within a hash or array.
6+
# Before passing the data to a type property or another function, it's useful
7+
# to be able to `unwrap` these values first. This function does this. If
8+
# sensitive data was included in the data, the whole result is then rewrapped
9+
# as Sensitive.
10+
#
11+
# Optionally, this function can be passed a block. When a block is given, it will
12+
# be run with the unwrapped data, but before the final rewrapping. This is useful
13+
# to provide transparent rewrapping to other functions in stdlib especially.
14+
#
15+
# This is analogous to the way `epp` transparently handles sensitive parameters.
16+
Puppet::Functions.create_function(:'stdlib::rewrap_sensitive_data') do
17+
# @param data The data
18+
# @param block A lambda that will be run after the data has been unwrapped, but before it is rewrapped, (if it contained sensitives)
19+
# @return Returns the rewrapped data
20+
dispatch :rewrap_sensitive_data do
21+
param 'Any', :data
22+
optional_block_param 'Callable[Any]', :block
23+
return_type 'Any'
24+
end
25+
26+
def rewrap_sensitive_data(data)
27+
@contains_sensitive = false
28+
29+
unwrapped = deep_unwrap(data)
30+
31+
result = block_given? ? yield(unwrapped) : unwrapped
32+
33+
if @contains_sensitive
34+
Puppet::Pops::Types::PSensitiveType::Sensitive.new(result)
35+
else
36+
result
37+
end
38+
end
39+
40+
def deep_unwrap(obj)
41+
case obj
42+
when Hash
43+
obj.each_with_object({}) do |(key, value), result|
44+
if key.is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive)
45+
# This situation is probably fairly unlikely in reality, but easy enough to support
46+
@contains_sensitive = true
47+
key = key.unwrap
48+
end
49+
result[key] = deep_unwrap(value)
50+
end
51+
when Array
52+
obj.map { |element| deep_unwrap(element) }
53+
when Puppet::Pops::Types::PSensitiveType::Sensitive
54+
@contains_sensitive = true
55+
deep_unwrap(obj.unwrap)
56+
else
57+
obj
58+
end
59+
end
60+
end

Diff for: lib/puppet/functions/stdlib/to_json_pretty.rb

+5-3
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ def to_json_pretty(data, skip_undef = false, opts = nil)
6767
end
6868

6969
data = data.compact if skip_undef && (data.is_a?(Array) || Hash)
70-
# Call ::JSON to ensure it references the JSON library from Ruby's standard library
71-
# instead of a random JSON namespace that might be in scope due to user code.
72-
JSON.pretty_generate(data, opts) << "\n"
70+
call_function('stdlib::rewrap_sensitive_data', data) do |unwrapped_data|
71+
# Call ::JSON to ensure it references the JSON library from Ruby's standard library
72+
# instead of a random JSON namespace that might be in scope due to user code.
73+
::JSON.pretty_generate(unwrapped_data, opts) << "\n"
74+
end
7375
end
7476
end

Diff for: spec/functions/rewrap_sensitive_data_spec.rb

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe 'stdlib::rewrap_sensitive_data' do
6+
it { is_expected.not_to be_nil }
7+
8+
context 'when called with data containing no sensitive elements' do
9+
it { is_expected.to run.with_params({}).and_return({}) }
10+
it { is_expected.to run.with_params([]).and_return([]) }
11+
it { is_expected.to run.with_params('a_string').and_return('a_string') }
12+
it { is_expected.to run.with_params(42).and_return(42) }
13+
it { is_expected.to run.with_params(true).and_return(true) }
14+
it { is_expected.to run.with_params(false).and_return(false) }
15+
16+
it { is_expected.to run.with_params({ 'foo' => 'bar' }).and_return({ 'foo' => 'bar' }) }
17+
end
18+
19+
context 'when called with a hash containing a sensitive string' do
20+
it 'unwraps the sensitive string and returns a sensitive hash' do
21+
is_expected.to run.with_params(
22+
{
23+
'username' => 'my_user',
24+
'password' => sensitive('hunter2')
25+
},
26+
).and_return(sensitive(
27+
{
28+
'username' => 'my_user',
29+
'password' => 'hunter2'
30+
},
31+
))
32+
end
33+
end
34+
35+
context 'when called with data containing lots of sensitive elements (including nested in arrays, and sensitive hashes etc)' do
36+
it 'recursively unwraps everything and marks the whole result as sensitive' do
37+
is_expected.to run.with_params(
38+
{
39+
'a' => sensitive('bar'),
40+
'b' => [
41+
1,
42+
2,
43+
:undef,
44+
true,
45+
false,
46+
{
47+
'password' => sensitive('secret'),
48+
'weird_example' => sensitive({ 'foo' => sensitive(42) }) # A sensitive hash containing a sensitive Int as the value to a hash contained in an array which is the value of a hash key...
49+
},
50+
],
51+
'c' => :undef,
52+
'd' => [],
53+
'e' => true,
54+
'f' => false,
55+
},
56+
).and_return(sensitive(
57+
{
58+
'a' => 'bar',
59+
'b' => [
60+
1,
61+
2,
62+
:undef,
63+
true,
64+
false,
65+
{
66+
'password' => 'secret',
67+
'weird_example' => { 'foo' => 42 }
68+
},
69+
],
70+
'c' => :undef,
71+
'd' => [],
72+
'e' => true,
73+
'f' => false,
74+
},
75+
))
76+
end
77+
end
78+
79+
context 'when a hash _key_ is sensitive' do
80+
it 'unwraps the key' do
81+
is_expected.to run.with_params(
82+
{
83+
sensitive('key') => 'value',
84+
},
85+
).and_return(sensitive(
86+
{
87+
'key' => 'value',
88+
},
89+
))
90+
end
91+
end
92+
93+
context 'when called with a block' do
94+
context 'that upcases hash values' do
95+
it do
96+
is_expected.to run
97+
.with_params({ 'secret' => sensitive('hunter2') })
98+
.with_lambda { |data| data.transform_values { |value| value.upcase } }
99+
.and_return(sensitive({ 'secret' => 'HUNTER2' }))
100+
end
101+
end
102+
context 'that converts data to yaml' do
103+
it do
104+
is_expected.to run
105+
.with_params({ 'secret' => sensitive('hunter2') })
106+
.with_lambda { |data| data.to_yaml }
107+
.and_return(sensitive("---\nsecret: hunter2\n"))
108+
end
109+
end
110+
end
111+
end

Diff for: spec/functions/to_json_pretty_spec.rb

+4
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@
2828
pending('Current implementation only elides nil values for hashes of depth=1')
2929
expect(subject).to run.with_params({ 'omg' => { 'lol' => nil }, 'what' => nil }, true).and_return("{\n}\n")
3030
}
31+
32+
context 'with data containing sensitive' do
33+
it { is_expected.to run.with_params('key' => sensitive('value')).and_return(sensitive("{\n \"key\": \"value\"\n}\n")) }
34+
end
3135
end

0 commit comments

Comments
 (0)