Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #37418 - Fixes an issue that caused hidden Ansible variables to be shown in plain text on the Host-Details page #717

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/controllers/api/v2/ansible_inventories_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ def schedule_params

def show_inventory(ids_key, condition_key)
ids = params.fetch(ids_key, []).uniq
render :json => ForemanAnsible::InventoryCreator.new(Host.where(condition_key => ids)).to_hash.to_json
if Foreman::Cast.to_bool(params[:redact_secrets]) || !User.current.can?(:edit_ansible_variables)
render :json => ForemanAnsible::InventoryCreator.new(Host.where(condition_key => ids)).to_hash_with_secrets_redacted.to_json
else
render :json => ForemanAnsible::InventoryCreator.new(Host.where(condition_key => ids)).to_hash.to_json
end
end
end
end
Expand Down
8 changes: 7 additions & 1 deletion app/controllers/api/v2/ansible_variables_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ def show; end
param_group :search_and_pagination, ::Api::V2::BaseController
add_scoped_search_description_for(AnsibleVariable)
def index
@ansible_variables = resource_scope_for_index
vars = resource_scope_for_index
unless User.current.can?(:edit_ansible_variables)
vars.each do |v|
v.value = v.hidden_value? ? v.hidden_value : v.value
end
end
@ansible_variables = vars
end

api :DELETE, '/ansible_variables/:id', N_('Deletes Ansible variable')
Expand Down
23 changes: 19 additions & 4 deletions app/graphql/presenters/overriden_ansible_variable_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,33 @@ module Presenters
class OverridenAnsibleVariablePresenter
attr_reader :ansible_variable

delegate :id, :key, :description, :override?,
:parameter_type, :hidden_value?, :omit, :required,
:validator_type, :validator_rule, :default_value,
delegate :id, :key, :description,
:parameter_type, :omit, :required,
:validator_type, :validator_rule,
:ansible_role, :current_value, :to => :ansible_variable
def hidden_value
ansible_variable.hidden_value?
end

def override
ansible_variable.override?
end
Comment on lines +13 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are we using this value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently nowhere. This is added by #716
You mentioned removing unnecessary fields. This one could be removed, but it will have to be done in the other PR.


def initialize(ansible_variable, override_resolver)
@ansible_variable = ansible_variable
@override_resolver = override_resolver
end

def default_value
ansible_variable.editable_by_user? ? ansible_variable.default_value : '*****'
Thorben-D marked this conversation as resolved.
Show resolved Hide resolved
end

def current_value
@override_resolver.resolve @ansible_variable
resolved = @override_resolver.resolve @ansible_variable
unless resolved.nil?
resolved[:value] = '*****' unless ansible_variable.editable_by_user?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we can use ansible_variable.hidden_value instead of the asterisks.

end
Thorben-D marked this conversation as resolved.
Show resolved Hide resolved
resolved
end
end
end
10 changes: 6 additions & 4 deletions app/services/foreman_ansible/ansible_info.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
module ForemanAnsible
class AnsibleInfo < ::HostInfo::Provider
def host_info
{ 'parameters' => ansible_params }
def host_info(redact_secrets = false)
{ 'parameters' => ansible_params(redact_secrets) }
end

def ansible_params
def ansible_params(redact_secrets = false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the value of redact_secrets is based on the user's permissions, why do we need to pass redact_secrets to this method? Can't we check if the user has the necessary permissions directly within the method instead?

variables = AnsibleVariable.where(:ansible_role_id => host.all_ansible_roles.pluck(:id), :override => true)
values = variables.values_hash(host)

variables.each_with_object({}) do |var, memo|
value = values[var]
memo[var.key] = value unless value.nil?
unless value.nil?
memo[var.key] = redact_secrets && var.hidden_value? ? var.hidden_value : value
end
memo
end
end
Expand Down
18 changes: 11 additions & 7 deletions app/services/foreman_ansible/inventory_creator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,32 @@ def initialize(hosts, template_invocation = nil)
# more advanced cases). Therefore we have only the 'all' group
# with all hosts.
def to_hash
to_hash_with_secrets_redacted(false)
end

def to_hash_with_secrets_redacted(redact_secrets = true)
Thorben-D marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a need to add this new method if it’s just duplicating the content of to_hash. It seems to complicate things unnecessarily. I suggest simply adding the redact_secrets parameter to the original method and making the necessary changes there.

hosts = @hosts.map(&:name)

{ 'all' => { 'hosts' => hosts,
'vars' => template_inputs(@template_invocation) },
'_meta' => { 'hostvars' => hosts_vars } }
'_meta' => { 'hostvars' => hosts_vars(redact_secrets) } }
end

def hosts_vars
def hosts_vars(redact_secrets = false)
hosts.reduce({}) do |hash, host|
hash.update(
host.name => host_vars(host)
host.name => host_vars(host, redact_secrets)
)
end
end

def host_vars(host)
def host_vars(host, redact_secrets = false)
{
'foreman' => reduced_host_info(host).fetch('parameters', {}),
'foreman_ansible_roles' => host_roles(host)
}.merge(connection_params(host)).
merge(host_params(host)).
merge(ansible_params(host))
merge(ansible_params(host, redact_secrets))
end

def connection_params(host)
Expand All @@ -62,8 +66,8 @@ def host_roles(host)
host.all_ansible_roles.map(&:name)
end

def ansible_params(host)
ForemanAnsible::AnsibleInfo.new(host).ansible_params
def ansible_params(host, redact_secrets = false)
ForemanAnsible::AnsibleInfo.new(host).ansible_params(redact_secrets)
end

def reduced_host_info(host)
Expand Down
1 change: 0 additions & 1 deletion app/views/ansible_roles/import.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<% title _("Changed Ansible roles") %>
<%= webpacked_plugins_js_for :foreman_ansible %>
<%= webpacked_plugins_css_for :foreman_ansible %>


<%= react_component(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<%= webpacked_plugins_js_for :foreman_ansible %>
<%= webpacked_plugins_css_for :foreman_ansible %>

<div class='tab-pane' id='ansible_roles'>
<% class_name = f.object.is_a?(Hostgroup) ? 'Hostgroup' : 'Host' %>
Expand Down
2 changes: 2 additions & 0 deletions app/views/foreman_ansible/job_templates/convert_to_rhel.erb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ kind: job_template
%>
---
- hosts: all
environment:
CONVERT2RHEL_THROUGH_FOREMAN: 1
tasks:
- name: Install convert2rhel
ansible.builtin.package:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import AnsibleHostInventory from './AnsibleHostInventory';
import ErrorState from '../../../ErrorState';

const WrappedAnsibleHostInventory = ({ hostId }) => {
const params = useMemo(() => ({ params: { host_ids: [hostId] } }), [hostId]);
const params = useMemo(
() => ({ params: { host_ids: [hostId], redact_secrets: true } }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we always sending redact_secrets: true? Shouldn't we allow users with the appropriate permissions to view the hidden values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the inventory, I am unsure what's best. As opposed to the variables, there is no action to toggle the visibility of hidden variables. I chose to hide hidden variables in the inventory for everyone, as permitted users are able to view those values under the "Variables" tab. I also feel like showing the values by default sort of defeats the purpose of hiding them.
Though, I can change this if you disagree.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned, I prefer not to change the current behavior where authorized users can view hidden values in the inventory. Consider a scenario where someone is using Foreman with a script that makes API calls to retrieve the inventory and relies on those variable values. If we change this behavior, it could break their setup. However, if you believe your approach is better, feel free to initiate a discussion in the community to gather more opinions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should note that the variables are only hidden if redact_secrets=true is passed during the API call. That is also the reason I have to pass this value through a couple of functions. This only happens in the UI.

[hostId]
);

const url = hostId && foremanUrl('/ansible/api/ansible_inventories/hosts');
const { response: inventory, status } = useAPI('get', url, params);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { TextInput } from '@patternfly/react-core';
import { TimesIcon, CheckIcon } from '@patternfly/react-icons';
import { sprintf, translate as __ } from 'foremanReact/common/I18n';

Expand All @@ -22,6 +23,21 @@ export const formatValue = variable => {
? variable.currentValue.value
: variable.defaultValue;

if (variable.hiddenValue) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the hiddenValue necessary here, or could we rely on variable.meta.canEdit instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, the hiddenValue field is needed to decide whether the "fake" variable should be shown instead of the real one. Relying on canEdit here would not work, as a permitted user can edit hidden and "not-hidden" variables.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, this block isn't necessary. If the user is authorized to view hidden values, we can simply display them as is. If they aren’t authorized, the value should already be hidden by the backend before it's sent.

return (
<TextInput
value="Not the secrets you're looking for..."
type="password"
aria-label="hidden ansible variable"
isDisabled
style={{
'--pf-c-form-control--BackgroundColor': 'white',
color: 'black',
}}
/>
);
}

switch (variable.parameterType) {
case 'boolean':
return value ? <CheckIcon /> : <TimesIcon />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const withFqdnOverride = canEdit => ({
validatorType: '',
validatorRule: null,
required: false,
hiddenValue: false,
lookupValues: {
nodes: [
{
Expand Down Expand Up @@ -70,6 +71,7 @@ const withDomainOverride = canEdit => ({
validatorType: '',
validatorRule: null,
required: false,
hiddenValue: false,
lookupValues: {
nodes: [],
},
Expand Down Expand Up @@ -142,6 +144,7 @@ export const mocks = [
validatorType: 'list',
validatorRule: 'a,b,c',
required: true,
hiddenValue: false,
lookupValues: {
nodes: [
{
Expand Down Expand Up @@ -170,6 +173,7 @@ export const mocks = [
validatorType: '',
validatorRule: null,
required: false,
hiddenValue: false,
lookupValues: {
nodes: [],
},
Expand All @@ -190,6 +194,7 @@ export const mocks = [
validatorType: '',
validatorRule: null,
required: false,
hiddenValue: false,
lookupValues: {
nodes: [],
},
Expand All @@ -215,6 +220,7 @@ export const mocks = [
validatorType: '',
validatorRule: null,
required: false,
hiddenValue: true,
lookupValues: {
nodes: [],
},
Expand All @@ -240,6 +246,7 @@ export const mocks = [
validatorType: '',
validatorRule: null,
required: false,
hiddenValue: false,
lookupValues: {
nodes: [],
},
Expand All @@ -260,6 +267,7 @@ export const mocks = [
validatorType: '',
validatorRule: null,
required: false,
hiddenValue: true,
lookupValues: {
nodes: [],
},
Expand All @@ -282,6 +290,7 @@ export const mocks = [
validatorType: '',
validatorRule: null,
required: false,
hiddenValue: true,
lookupValues: {
nodes: [],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,20 @@ describe('AnsibleVariableOverrides', () => {
const actions = screen.queryAllByRole('button', { name: 'Actions' });
expect(actions).toHaveLength(0);
});
it('should hide hidden values', async () => {
const { container } = render(
<TestComponent
hostId={hostId}
mocks={mocks}
hostAttrs={hostAttrs}
history={historyMock}
/>
);
await waitFor(tick);
expect(screen.getByText('ellipse')).toBeInTheDocument();
expect(screen.getByText('sun')).toBeInTheDocument();
expect(screen.getByText('moon')).toBeInTheDocument();
// number of hidden variables + 1 for pagination input
expect(container.getElementsByTagName('input')).toHaveLength(3 + 1);
});
});
1 change: 1 addition & 0 deletions webpack/graphql/queries/hostVariableOverrides.gql
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ query($id: String!, $match: String, $first: Int, $last: Int) {
validatorType
validatorRule
required
hiddenValue
lookupValues(match: $match) {
nodes {
id
Expand Down
Loading