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

Do not put the subscription-manager password onto the command line. #492

Merged
merged 11 commits into from May 27, 2022

Conversation

abadger
Copy link
Member

@abadger abadger commented May 10, 2022

Passing values on the command line is insecure. With this change,
the rhsm password is passed interactively to subscription-manager
instead of being passed on the commandline when we shell out to it.

The structure of this change deserves a bit of description. Previously,
we called one function to assemble all of the information needed to
invoke subscription-manager and then returned that as a string that
could be run as a command line. We called a second function with that
string to actually run the command.

To send the password interactively, we need to stop adding the password
to the string of command line arguments but it still makes sense to keep
the code that figures out the password together with the code which
finds the other command line args. So it makes sense to keep a single
function to do that but return the password and other args separately.

We could use a dict, a class, or a tuple as the return value from the
function. That doesn't feel too ugly. But then we need to pass that
structure into the function which takes care of invoking
subscription-manager on the command line and that does feel ugly.
That function would have to care about the structure of the data we pass
in (If a tuple, what is the order? If a dict, what are the field
names?, etc). To take care of this, we can make the data structure that
we return from assembling the data a class and the function which calls
subscription-manager a method of that class because it's quite natural
for a method to have knowledge of what attributes the class contains.

Hmm... but now that we have a class with behaviours (methods), it starts
to feel like we could do some more things. A function that fills in the
values of a class, validates that the data is proper, and then returns
an instance of that class is really a constructor, right? So it makes
sense to move the function which assembles the data and returns the
class a constructor. But that particular function isn't very generic:
it uses knowledge of our global toolopts.tool_opts to populate the
class. So let's write a simple init() that takes all of the values
needed as separate parameters and then create an alternative constructor
(an @classmethod which returns an instance of the class) which gets the
data from a ToolOpt, and then calls init() with those values and
returns the resulting class.

Okay, things are much cleaner now, but there's one more thing that we
can do. We now have a class that has a constructor to read in data and
a single method that we can call to return some results. What do we
call an object that can be called to return a result? A function or
more generically, in python, a Callable. We can turn this class into a
callable by renaming the method which actually invokes
subscription-manager call().

What we have at the end of all this is a way to create a function which
knows about the settings in tool_opts which we can then call to perform
our subscription-manager needs::

registration_command = RegistrationCommand.from_tool_opts()
return_code = registration_command()

OAMG-6551 #done convert2rhel now passes the rhsm password to subscription-manager securely.

  • Modify the hiding of secret to hide both --password SECRET and
    --password=SECRET. Currently we only use it with passwords that we
    are passing in the second form (to subscription-manager) but catching
    both will future proof us in case we use this function for more things
    in the future. (Eric Gustavsson)

    • Note: using generator expressions was tried here but found that they
      only bind the variable being iterated over at definition time, the
      rest of the variables are bound when the generator is used. This
      means that constructing a set of generators in a loop doesn't really
      work as the loop variables that you use in the generator will have a
      different value by the time you're done.

      So a nested for loop and if-else is the way to implement this.

  • Add a comment about activation_key being insecure for now. We can fix
    this once subscription-manager implements https://issues.redhat.com/browse/ENT-4724
    (Eric Gustavsson)

Unittest enhancements for the subscription RegisterCommand change:

  • Add global_tool_opts fixture to conftest.py which monkeypatches a
    fresh ToolOpts into convert2rhel.toolopts.tool_opts. That way tests
    can modify that without worrying about polluting other tests.

    • How toolopts is imported in the code makes a difference whether this
      fixture is sufficient or if you need to do a little more work. If
      the import is::

      from convert2rhel import toolopts
      do_something(toolopts.tool_opts.username)

    then your test can just do::

    def test_thing_that_uses_toolopts(global_tool_opts):
        global_tool_opts.username = 'badger'
    

    Most of our code, though, imports like this::

    # In subscription.py, for instance
    from convert2rhel.toolopts import tool_opts
    do_something(tool_opts.username)
    

    so the tests should do something like this::

    def test_toolopts_differently(global_test_opts, monkeypatch):
        monkeypatch.setattr(subscription, 'tool_opts', global_tool_opts)
    
  • Add unittests for utils.run_cmd_in_pty()

  • Add unittests for the subscription.RegistrationCommand object.

  • subscription_test.py::test_hide_secrets was expanded to test without
    equals sign

  • Flaky test appeared within systeminfo_test.py::test_generate_rpm_va
    due to changes to tool_opts in other tests. A temporary fix is added
    until the whole test class is changed from unittest to pytest

  • Fix failing unittests for RegistrationCommand

  • Sometimes a process will close stdout before it is done processing.
    When that happens, we need to wait() for the process to end before
    closing the pty. If we don't wait, the process will receive a HUP
    signal which may end it before it is done.

    • But on RHEL7, pexpect.wait() will throw an exception if you wait on
      an already finished process. So we need to ignore that exception
      there.
  • Add integration test Check for cve fixes, add 'psutil' to
    install-testing-depends playbook

  • Fix Pexpect.spawn truncating lines on RHEL7.
    Along with a comment on why the change fixes the bug.

Fixes: https://issues.redhat.com/browse/RHELC-432

@codecov
Copy link

codecov bot commented May 10, 2022

Codecov Report

Merging #492 (960490a) into main (8658831) will increase coverage by 1.19%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##             main     #492      +/-   ##
==========================================
+ Coverage   82.46%   83.65%   +1.19%     
==========================================
  Files          16       16              
  Lines        2258     2319      +61     
  Branches      383      403      +20     
==========================================
+ Hits         1862     1940      +78     
+ Misses        331      314      -17     
  Partials       65       65              
Impacted Files Coverage Δ
convert2rhel/subscription.py 83.80% <100.00%> (+3.27%) ⬆️
convert2rhel/utils.py 69.69% <100.00%> (+5.91%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8658831...960490a. Read the comment docs.

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 10, 2022

This pull request introduces 3 alerts and fixes 1 when merging 18edac5 into 23fe45a - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@abadger
Copy link
Member Author

abadger commented May 10, 2022

I believe the lgtm errors are false positives but I would appreciate it if someone else would take a look as lgtm is flagging them as security issues (revealing of private data).

class PexpectSizedWindowSpawn(pexpect.spawn):
# https://github.com/pexpect/pexpect/issues/134
def setwinsize(self, rows, cols):
super(PexpectSizedWindowSpawn, self).setwinsize(rows, 120)
Copy link
Member

Choose a reason for hiding this comment

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

We're removing this workaround in #475 for 0.27 and won't fix it for 0.26

@SpyTec
Copy link
Member

SpyTec commented May 10, 2022

@abadger was there any difference between this and the patch?

Copy link
Member

@SpyTec SpyTec left a comment

Choose a reason for hiding this comment

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

The LGTM warnings seem like false positives

convert2rhel/subscription.py Outdated Show resolved Hide resolved
convert2rhel/unit_tests/subscription_test.py Outdated Show resolved Hide resolved
@abadger
Copy link
Member Author

abadger commented May 10, 2022

@abadger was there any difference between this and the patch?

There were quite a few conflicts (there have been changes from Rodolfo and Andrew which were merged after the patch was created) which I had to resolve. (I have asked both of them to take a look at this PR specifically to see whether their changes are still intact.)

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 10, 2022

This pull request introduces 3 alerts and fixes 1 when merging 12dc446 into 23fe45a - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 10, 2022

This pull request introduces 3 alerts and fixes 1 when merging c2a1150 into 23fe45a - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 10, 2022

This pull request introduces 3 alerts and fixes 1 when merging 68e7ad8 into 23fe45a - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 10, 2022

This pull request introduces 3 alerts and fixes 1 when merging 085fb11 into 23fe45a - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@r0x0d
Copy link
Member

r0x0d commented May 10, 2022

@abadger was there any difference between this and the patch?

There were quite a few conflicts (there have been changes from Rodolfo and Andrew which were merged after the patch was created) which I had to resolve. (I have asked both of them to take a look at this PR specifically to see whether their changes are still intact.)

Looking at the changes right now.

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 10, 2022

This pull request introduces 3 alerts and fixes 1 when merging b26c1c6 into 23fe45a - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@SpyTec SpyTec requested review from r0x0d and Andrew-ang9 May 11, 2022 09:06
r0x0d
r0x0d previously approved these changes May 11, 2022
Copy link
Member

@r0x0d r0x0d left a comment

Choose a reason for hiding this comment

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

@abadger most of the changes I proposed here are more aesthetic ones, thus, feel free to accept or deny them!

In an overral, from what I remember from my code in subscription.py, everything looks fine with the modifications and merges you did, don't think you missed anything!

convert2rhel/subscription.py Outdated Show resolved Hide resolved
convert2rhel/subscription.py Outdated Show resolved Hide resolved
convert2rhel/subscription.py Outdated Show resolved Hide resolved
convert2rhel/subscription.py Outdated Show resolved Hide resolved
convert2rhel/subscription.py Outdated Show resolved Hide resolved
convert2rhel/utils.py Outdated Show resolved Hide resolved
convert2rhel/utils.py Outdated Show resolved Hide resolved
plans/tier0.fmf Show resolved Hide resolved
plans/tier0.fmf Outdated Show resolved Hide resolved
tests/integration/tier0/check-cve-2022-1662/main.fmf Outdated Show resolved Hide resolved
@r0x0d
Copy link
Member

r0x0d commented May 11, 2022

Note: it was not an approval, I selected the comment option when submitting the review. Don't know what went wrong.

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 11, 2022

This pull request introduces 3 alerts and fixes 1 when merging 7cbe6a8 into 37b3a00 - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 11, 2022

This pull request introduces 3 alerts and fixes 1 when merging 0bf9ebb into 37b3a00 - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 11, 2022

This pull request introduces 3 alerts and fixes 1 when merging 5671a94 into 37b3a00 - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

bocekm
bocekm previously approved these changes May 11, 2022
@abadger
Copy link
Member Author

abadger commented May 12, 2022

Rebased to pick up the fixes for ubi8

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 12, 2022

This pull request introduces 3 alerts and fixes 1 when merging e84faff into 68bf0ac - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 12, 2022

This pull request introduces 3 alerts and fixes 1 when merging 29ab9b8 into 68bf0ac - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 16, 2022

This pull request introduces 3 alerts and fixes 1 when merging 36e0e98 into a921086 - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 16, 2022

This pull request introduces 3 alerts and fixes 1 when merging e9816db into a921086 - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 17, 2022

This pull request introduces 3 alerts and fixes 1 when merging 34fcd31 into a921086 - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@danmyway
Copy link
Member

All tests have passed, @bocekm

bocekm
bocekm previously approved these changes May 26, 2022
@r0x0d
Copy link
Member

r0x0d commented May 26, 2022

@abadger +1 for that toolopts fixture.

r0x0d
r0x0d previously approved these changes May 26, 2022
abadger and others added 11 commits May 26, 2022 07:31
Fixes CVE-2022-0852

Passing values on the command line is insecure.  With this change,
the rhsm password is passed interactively to subscription-manager
instead of being passed on the commandline when we shell out to it.

The structure of this change deserves a bit of description.  Previously,
we called one function to assemble all of the information needed to
invoke subscription-manager and then returned that as a string that
could be run as a command line.  We called a second function with that
string to actually run the command.

To send the password interactively, we need to stop adding the password
to the string of command line arguments but it still makes sense to keep
the code that figures out the password together with the code which
finds the other command line args.  So it makes sense to keep a single
function to do that but return the password and other args separately.

We could use a dict, a class, or a tuple as the return value from the
function.  That doesn't feel too ugly.  But then we need to pass that
structure into the function which takes care of invoking
subscription-manager on the command line and that *does* feel ugly.
That function would have to care about the structure of the data we pass
in (If a tuple, what is the order?  If a dict, what are the field
names?, etc).  To take care of this, we can make the data structure that
we return from assembling the data a class and the function which calls
subscription-manager a method of that class because it's quite natural
for a method to have knowledge of what attributes the class contains.

Hmm... but now that we have a class with behaviours (methods), it starts
to feel like we could do some more things.  A function that fills in the
values of a class, validates that the data is proper, and then returns
an instance of that class is really a constructor, right?  So it makes
sense to move the function which assembles the data and returns the
class a constructor.  But that particular function isn't very generic:
it uses knowledge of our global toolopts.tool_opts to populate the
class.  So let's write a simple __init__() that takes all of the values
needed as separate parameters and then create an alternative constructor
(an @classmethod which returns an instance of the class) which gets the
data from a ToolOpt, and then calls __init__() with those values and
returns the resulting class.

Okay, things are much cleaner now, but there's one more thing that we
can do.  We now have a class that has a constructor to read in data and
a single method that we can call to return some results.  What do we
call an object that can be called to return a result?  A function or
more generically, in python, a Callable.  We can turn this class into a
callable by renaming the method which actually invokes
subscription-manager __call__().

What we have at the end of all this is a way to create a function which
knows about the settings in tool_opts which we can then call to perform
our subscription-manager needs::

    registration_command = RegistrationCommand.from_tool_opts()
    return_code = registration_command()

OAMG-6551 #done  convert2rhel now passes the rhsm password to subscription-manager securely.

* Modify the hiding of secret to hide both --password SECRET and
  --password=SECRET.  Currently we only use it with passwords that we
  are passing in the second form (to subscription-manager) but catching
  both will future proof us in case we use this function for more things
  in the future. (Eric Gustavsson)
  * Note: using generator expressions was tried here but found that they
    only bind the variable being iterated over at definition time, the
    rest of the variables are bound when the generator is used.  This
    means that constructing a set of generators in a loop doesn't really
    work as the loop variables that you use in the generator will have a
    different value by the time you're done.

    So a nested for loop and if-else is the way to implement this.

* Add a comment about activation_key being insecure for now.  We can fix
  this once subscription-manager implements https://issues.redhat.com/browse/ENT-4724
  (Eric Gustavsson)

Unittest enhancements for the subscription RegisterCommand change:

* Add global_tool_opts fixture to conftest.py which monkeypatches a
  fresh ToolOpts into convert2rhel.toolopts.tool_opts.  That way tests
  can modify that without worrying about polluting other tests.

  * How toolopts is imported in the code makes a difference whether this
    fixture is sufficient or if you need to do a little more work. If
    the import is::

      from convert2rhel import toolopts
      do_something(toolopts.tool_opts.username)

   then your test can just do::

      def test_thing_that_uses_toolopts(global_tool_opts):
          global_tool_opts.username = 'badger'

   Most of our code, though, imports like this::

      # In subscription.py, for instance
      from convert2rhel.toolopts import tool_opts
      do_something(tool_opts.username)

   so the tests should do something like this::

      def test_toolopts_differently(global_test_opts, monkeypatch):
          monkeypatch.setattr(subscription, 'tool_opts', global_tool_opts)

* Add unittests for utils.run_cmd_in_pty()
* Add unittests for the subscription.RegistrationCommand object.
* subscription_test.py::test_hide_secrets was expanded to test without
  equals sign
* Flaky test appeared within systeminfo_test.py::test_generate_rpm_va
  due to changes to tool_opts in other tests. A temporary fix is added
  until the whole test class is changed from unittest to pytest
* Fix failing unittests for RegistrationCommand

* Sometimes a process will close stdout before it is done processing.
  When that happens, we need to wait() for the process to end before
  closing the pty.  If we don't wait, the process will receive a HUP
  signal which may end it before it is done.
  * But on RHEL7, pexpect.wait() will throw an exception if you wait on
    an already finished process.  So we need to ignore that exception
    there.

* Add integration test Check for cve fixes, add 'psutil' to
  install-testing-depends playbook

* Fix Pexpect.spawn truncating lines on RHEL7.
  Along with a comment on why the change fixes the bug.
lgtm is flagging some cases where it thinks we are logging sensitive
data.  Here's why we're ignoring lgtm:

* One case logs the username to the terminal as a hint for the user as
  to what account is being used.  We consider username to not be
  sensitive.
* One case logs the subscription-manager invocation.  We run the command
  line through hide_secrets() to remove private information when we do
  this.
* The last case logs which argument was passed to subscription-manager
  without a secret attached to it.  ie: "--password" is passed to
  subscription-manager without a password being added afterwards.  In
  this case, the string "--password" will be logged.
* Use a regular expression to find either "Password: " or "password: "
  when waiting for subscription-manager to prompt for the rhsm password.
* Fix unittests that were using Mock.called_once_with().  That doesn't exist
  so the Mock() was translating it to a function that did nothing.
  Instead, use assert_called_once_with() which will assert if the Mock
  object was not called with the proper parameters.
Now that we know the cve number, use it in the name of the integration
test.
Add docstrings for the RegistrationCommand.args properyy and
hide_secrets() function
…aks.

gitleaks scans the repository for passwords which might have been
committed by accident.  Adding EXAMPLE to the password string should
allow gitleaks to recognize this as test data rather than a valid
credential.
Testing farm doesn't have python-devel installed.
We need that to install psutil needed as a testing dependency.

Edit test_user_response, as one of the child.expect_exact() is not
configured right.
Would need to be made a bit more generic before making it available to
all tests.
gitleaks continues to warn about passwords in the repo.  Use .gitleaks
to specify that the passwords in the test file are not valid and should
not be warned about.
@abadger abadger dismissed stale reviews from r0x0d and bocekm via 960490a May 26, 2022 14:34
@lgtm-com
Copy link
Contributor

lgtm-com bot commented May 26, 2022

This pull request introduces 3 alerts and fixes 1 when merging 960490a into 8658831 - view on LGTM.com

new alerts:

  • 3 for Clear-text logging of sensitive information

fixed alerts:

  • 1 for Clear-text logging of sensitive information

@bocekm bocekm merged commit 8d72fb0 into oamg:main May 27, 2022
16 of 17 checks passed
r0x0d pushed a commit to r0x0d/convert2rhel that referenced this pull request Jun 1, 2022
…amg#492)

* Do not put the subscription-manager password onto the command line.

Fixes CVE-2022-0852

Passing values on the command line is insecure.  With this change,
the rhsm password is passed interactively to subscription-manager
instead of being passed on the commandline when we shell out to it.

The structure of this change deserves a bit of description.  Previously,
we called one function to assemble all of the information needed to
invoke subscription-manager and then returned that as a string that
could be run as a command line.  We called a second function with that
string to actually run the command.

To send the password interactively, we need to stop adding the password
to the string of command line arguments but it still makes sense to keep
the code that figures out the password together with the code which
finds the other command line args.  So it makes sense to keep a single
function to do that but return the password and other args separately.

We could use a dict, a class, or a tuple as the return value from the
function.  That doesn't feel too ugly.  But then we need to pass that
structure into the function which takes care of invoking
subscription-manager on the command line and that *does* feel ugly.
That function would have to care about the structure of the data we pass
in (If a tuple, what is the order?  If a dict, what are the field
names?, etc).  To take care of this, we can make the data structure that
we return from assembling the data a class and the function which calls
subscription-manager a method of that class because it's quite natural
for a method to have knowledge of what attributes the class contains.

Hmm... but now that we have a class with behaviours (methods), it starts
to feel like we could do some more things.  A function that fills in the
values of a class, validates that the data is proper, and then returns
an instance of that class is really a constructor, right?  So it makes
sense to move the function which assembles the data and returns the
class a constructor.  But that particular function isn't very generic:
it uses knowledge of our global toolopts.tool_opts to populate the
class.  So let's write a simple __init__() that takes all of the values
needed as separate parameters and then create an alternative constructor
(an @classmethod which returns an instance of the class) which gets the
data from a ToolOpt, and then calls __init__() with those values and
returns the resulting class.

Okay, things are much cleaner now, but there's one more thing that we
can do.  We now have a class that has a constructor to read in data and
a single method that we can call to return some results.  What do we
call an object that can be called to return a result?  A function or
more generically, in python, a Callable.  We can turn this class into a
callable by renaming the method which actually invokes
subscription-manager __call__().

What we have at the end of all this is a way to create a function which
knows about the settings in tool_opts which we can then call to perform
our subscription-manager needs::

    registration_command = RegistrationCommand.from_tool_opts()
    return_code = registration_command()

OAMG-6551 #done  convert2rhel now passes the rhsm password to subscription-manager securely.

* Modify the hiding of secret to hide both --password SECRET and
  --password=SECRET.  Currently we only use it with passwords that we
  are passing in the second form (to subscription-manager) but catching
  both will future proof us in case we use this function for more things
  in the future. (Eric Gustavsson)
  * Note: using generator expressions was tried here but found that they
    only bind the variable being iterated over at definition time, the
    rest of the variables are bound when the generator is used.  This
    means that constructing a set of generators in a loop doesn't really
    work as the loop variables that you use in the generator will have a
    different value by the time you're done.

    So a nested for loop and if-else is the way to implement this.

* Add global_tool_opts fixture to conftest.py which monkeypatches a
  fresh ToolOpts into convert2rhel.toolopts.tool_opts.  That way tests
  can modify that without worrying about polluting other tests.

  * How toolopts is imported in the code makes a difference whether this
    fixture is sufficient or if you need to do a little more work. If
    the import is::

      from convert2rhel import toolopts
      do_something(toolopts.tool_opts.username)

   then your test can just do::

      def test_thing_that_uses_toolopts(global_tool_opts):
          global_tool_opts.username = 'badger'

   Most of our code, though, imports like this::

      # In subscription.py, for instance
      from convert2rhel.toolopts import tool_opts
      do_something(tool_opts.username)

   so the tests should do something like this::

      def test_toolopts_differently(global_test_opts, monkeypatch):
          monkeypatch.setattr(subscription, 'tool_opts', global_tool_opts)

* Sometimes a process will close stdout before it is done processing.
  When that happens, we need to wait() for the process to end before
  closing the pty.  If we don't wait, the process will receive a HUP
  signal which may end it before it is done.
  * But on RHEL7, pexpect.wait() will throw an exception if you wait on
    an already finished process.  So we need to ignore that exception
    there.

lgtm is flagging some cases where it thinks we are logging sensitive
data.  Here's why we're ignoring lgtm:

* One case logs the username to the terminal as a hint for the user as
  to what account is being used.  We consider username to not be
  sensitive.
* One case logs the subscription-manager invocation.  We run the command
  line through hide_secrets() to remove private information when we do
  this.
* The last case logs which argument was passed to subscription-manager
  without a secret attached to it.  ie: "--password" is passed to
  subscription-manager without a password being added afterwards.  In
  this case, the string "--password" will be logged.

Testing farm doesn't have python-devel installed.
We need that to install psutil needed as a testing dependency.

Co-authored-by: Daniel Diblik <ddiblik@redhat.com>
@SpyTec SpyTec mentioned this pull request Jun 3, 2022
Andrew-ang9 pushed a commit to Andrew-ang9/convert2rhel that referenced this pull request Jun 17, 2022
…amg#492)

* Do not put the subscription-manager password onto the command line.

Fixes CVE-2022-0852

Passing values on the command line is insecure.  With this change,
the rhsm password is passed interactively to subscription-manager
instead of being passed on the commandline when we shell out to it.

The structure of this change deserves a bit of description.  Previously,
we called one function to assemble all of the information needed to
invoke subscription-manager and then returned that as a string that
could be run as a command line.  We called a second function with that
string to actually run the command.

To send the password interactively, we need to stop adding the password
to the string of command line arguments but it still makes sense to keep
the code that figures out the password together with the code which
finds the other command line args.  So it makes sense to keep a single
function to do that but return the password and other args separately.

We could use a dict, a class, or a tuple as the return value from the
function.  That doesn't feel too ugly.  But then we need to pass that
structure into the function which takes care of invoking
subscription-manager on the command line and that *does* feel ugly.
That function would have to care about the structure of the data we pass
in (If a tuple, what is the order?  If a dict, what are the field
names?, etc).  To take care of this, we can make the data structure that
we return from assembling the data a class and the function which calls
subscription-manager a method of that class because it's quite natural
for a method to have knowledge of what attributes the class contains.

Hmm... but now that we have a class with behaviours (methods), it starts
to feel like we could do some more things.  A function that fills in the
values of a class, validates that the data is proper, and then returns
an instance of that class is really a constructor, right?  So it makes
sense to move the function which assembles the data and returns the
class a constructor.  But that particular function isn't very generic:
it uses knowledge of our global toolopts.tool_opts to populate the
class.  So let's write a simple __init__() that takes all of the values
needed as separate parameters and then create an alternative constructor
(an @classmethod which returns an instance of the class) which gets the
data from a ToolOpt, and then calls __init__() with those values and
returns the resulting class.

Okay, things are much cleaner now, but there's one more thing that we
can do.  We now have a class that has a constructor to read in data and
a single method that we can call to return some results.  What do we
call an object that can be called to return a result?  A function or
more generically, in python, a Callable.  We can turn this class into a
callable by renaming the method which actually invokes
subscription-manager __call__().

What we have at the end of all this is a way to create a function which
knows about the settings in tool_opts which we can then call to perform
our subscription-manager needs::

    registration_command = RegistrationCommand.from_tool_opts()
    return_code = registration_command()

OAMG-6551 #done  convert2rhel now passes the rhsm password to subscription-manager securely.

* Modify the hiding of secret to hide both --password SECRET and
  --password=SECRET.  Currently we only use it with passwords that we
  are passing in the second form (to subscription-manager) but catching
  both will future proof us in case we use this function for more things
  in the future. (Eric Gustavsson)
  * Note: using generator expressions was tried here but found that they
    only bind the variable being iterated over at definition time, the
    rest of the variables are bound when the generator is used.  This
    means that constructing a set of generators in a loop doesn't really
    work as the loop variables that you use in the generator will have a
    different value by the time you're done.

    So a nested for loop and if-else is the way to implement this.

* Add global_tool_opts fixture to conftest.py which monkeypatches a
  fresh ToolOpts into convert2rhel.toolopts.tool_opts.  That way tests
  can modify that without worrying about polluting other tests.

  * How toolopts is imported in the code makes a difference whether this
    fixture is sufficient or if you need to do a little more work. If
    the import is::

      from convert2rhel import toolopts
      do_something(toolopts.tool_opts.username)

   then your test can just do::

      def test_thing_that_uses_toolopts(global_tool_opts):
          global_tool_opts.username = 'badger'

   Most of our code, though, imports like this::

      # In subscription.py, for instance
      from convert2rhel.toolopts import tool_opts
      do_something(tool_opts.username)

   so the tests should do something like this::

      def test_toolopts_differently(global_test_opts, monkeypatch):
          monkeypatch.setattr(subscription, 'tool_opts', global_tool_opts)

* Sometimes a process will close stdout before it is done processing.
  When that happens, we need to wait() for the process to end before
  closing the pty.  If we don't wait, the process will receive a HUP
  signal which may end it before it is done.
  * But on RHEL7, pexpect.wait() will throw an exception if you wait on
    an already finished process.  So we need to ignore that exception
    there.

lgtm is flagging some cases where it thinks we are logging sensitive
data.  Here's why we're ignoring lgtm:

* One case logs the username to the terminal as a hint for the user as
  to what account is being used.  We consider username to not be
  sensitive.
* One case logs the subscription-manager invocation.  We run the command
  line through hide_secrets() to remove private information when we do
  this.
* The last case logs which argument was passed to subscription-manager
  without a secret attached to it.  ie: "--password" is passed to
  subscription-manager without a password being added afterwards.  In
  this case, the string "--password" will be logged.

Testing farm doesn't have python-devel installed.
We need that to install psutil needed as a testing dependency.

Co-authored-by: Daniel Diblik <ddiblik@redhat.com>
@abadger abadger deleted the fix-sub-mgr-cli-call branch July 21, 2022 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants