Skip to content

Commit

Permalink
feat: Sessions can now be run in a Windows Service context (#97)
Browse files Browse the repository at this point in the history
feat(WIP): Sessions can now be run in a Windows Service context

Problem:

Windows requires the use of different Win32 APIs to impersonate users when
you're running in the context of a Service (Windows Session ID 0) than if
you're running in an interactive logon session. The current code uses
CreateProcessWithLogonW to do impersonation, and that API does not work inside Session 0.

Solution:

Detect when we are running in Session 0, and make use of CreateProcessAsUserW instead
in that case. To support this we require that when running in Session 0, the caller of
this library create the WindowsSessionUser using the handle to a logon token that they
create with the Win32 APIs rather than with a password directly.

Limitation / Known Issues:

Impersonation tests on Windows do not load the user's profile. We don't
have any tests that actually *need* it; the library expects is caller to
manage the logon token and profile load. Including a profile load in
tests was the source of another mysterious crash when we tried to unload
the profile.

Signed-off-by: Daniel Neilson <[email protected]>
  • Loading branch information
ddneilson authored Mar 5, 2024
1 parent 788a503 commit 72ff65b
Show file tree
Hide file tree
Showing 29 changed files with 1,615 additions and 661 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/reuse_python_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ jobs:
$plaintext_password = -join([char[]](48..122) | Get-Random -Count 16)
$password = ConvertTo-SecureString $plaintext_password -AsPlainText -Force
New-LocalUser -Name $username -Password $password
echo OJD_SESSIONS_USER_NAME=$username >> $env:GITHUB_ENV
echo OPENJD_TEST_WIN_USER_NAME=$username >> $env:GITHUB_ENV
echo "::add-mask::$plaintext_password"
echo OJD_SESSIONS_USER_PASSWORD=$plaintext_password >> $env:GITHUB_ENV
echo OPENJD_TEST_WIN_USER_PASSWORD=$plaintext_password >> $env:GITHUB_ENV
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
Expand Down
48 changes: 44 additions & 4 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,56 @@ within the `Session`.

## Testing

This package strives for very high test coverage of its functionality. You are asked to help us maintain our
high bar by adding thorough test coverage for any changes you make and any testing gaps that you discover.

To run our tests simply run: `hatch run test`

If you have multiple version of Python installed (e.g. Python 3.9, 3.10, 3.11, etc) then you can run the tests
against all of your installed versions of python with: `hatch run all:test`

### User Impersonation

This library contains functionality to run subprocesses as a user other than the one that is
running the main process. Scripting has been added to this repository to test this functionality
on Linux, and we have some unit tests that require being run in a specific docker container
that is set up for testing running subprocesses as different users.
running the main process. You will need to take special steps to ensure that your changes
keep this functionality running in tip-top shape.

#### User Impersonation: POSIX-Based Systems

To run the impersonation tests you must create additional users and groups for the impersonation
tests on your local system and then set environment variables before running the tests.

Scripting has been added to this repository to test this functionality on Linux using
docker containers that we have set up for this purpose.

To run these tests:
1. With users configured locally in /etc/passwd & /etc/groups: `scripts/run_sudo_tests.sh --build`
2. With users via an LDAP client: `scripts/run_sudo_tests.sh --build --ldap`

Please ensure that you run these tests if making modifications that may affect cross-user functionality.
If you are unable to use the provided docker container then you need to set up the `OPENJD_TEST_SUDO_*`
environment variables and their referenced users and groups as in the Dockerfile under
`testing_containers/localuser_sudo_environment/Dockerfile` in this repository.

#### User Impersonation: Windows-Based Systems

This library performs impersonation differently based on whether it is being run as part
of an OS Service (with Windows Session ID 0) or an interactive logon session (which has
Windows Session ID > 0). Thus, changes to the impersonation logic may need to be tested in
both of these environments.

To run the impersonation tests you will require a separate user on your workstation, and its
password, that you are able to logon as. Then:

1. Set the environment variable `OPENJD_TEST_WIN_USER_NAME` to the username of that user;
2. Set the environment variable `OPENJD_TEST_WIN_USER_PASSWORD` to that user's password; and
3. Then run the tests with `hatch run test` as normal.
* If done correctly, then you should not see any xfail tests related to impersonation.

Run these tests in both:
1. A terminal in your interactive logon session to test the impersonation logic when
Windows Session ID > 0; and
2. An `ssh` terminal into your workstation to test the impersonation logic when Windows
Session ID is 0.

## The Package's Public Interface

Expand Down
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This library requires:
3. On Linux/MacOS:
* `sudo`
4. On Windows:
* CPython implementation of Python
* PowerShell 5.x

**EXPERIMENTAL** Note that compatibility with the Windows operating system is currently in active development
Expand Down Expand Up @@ -193,6 +194,82 @@ with Session(
action_event.wait()
```

### Impersonating a User

This library supports running its Session Actions as a different operating system
user than the user that is running the library. In the following, we refer to the
operating system user that is running this library as the `host` user and the
user that is being impersonated to run actions as the `actions` user.

This feature exists to:
1. Provide a means to securely isolate the environment and files of the `host` user
from the environment in which the Session Actions are run. Configure your filesystem
permissions, and user groups for the `host` and `actions` users such that the `actions`
user cannot read, write, or execute any of the `host` user files that it should not be
able to.
2. Provide a way for you to permit access, on a per-Session basis, to specific local and
shared filesystem assets to the running Actions running in the Session.

#### Impersonating a User: POSIX Systems

To run an impersonated Session on POSIX Systems modify the "Running a Session" example
as follows:

```
...
from openjd.sessions import PosixSessionUser
...
user = PosixSessionUser($USERNAME$, password=$PASSWORD_OF_USERNAME$)
...
with Session(
session_id="demo",
job_parameter_values=job_parameters,
callback=action_complete_callback,
user=user
) as session:
...
```

You must ensure that the `host` user is able to run commands as the `actions` user
with passwordless `sudo` by, for example, adding a rule like follows to your
`sudoers` file or making the equivalent change in your user permissions directory:

```
host ALL=(actions) NOPASSWD: ALL
```

#### Impersonating a User: Windows Systems

To run an impersonated Session on Windows Systems modify the "Running a Session" example
as follows:

```
...
from openjd.sessions import WindowsSessionUser
...
# If you're running in an interactive logon session (e.g. cmd or powershell on your desktop)
user = WindowsSessionUser($USERNAME$, password=$PASSWORD_OF_USERNAME$)
# If you're running in a Windows Service
user = WindowsSessionUser($USERNAME$, logon_token=user_logon_token)
# Where `user_logon_token` is a token that you have created that is compatible
# with the Win32 API: CreateProcessAsUser
...
with Session(
session_id="demo",
job_parameter_values=job_parameters,
callback=action_complete_callback,
user=user
) as session:
...
```

If running in a Windows Service, then you must ensure that:
1. The `host` user is an Administrator, LocalSystem, or LocalService user as your
security posture requires; and
2. The `host` user has the [Replace a process level token](https://learn.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/replace-a-process-level-token)
privilege.


## Downloading

You can download this package from:
Expand Down
31 changes: 30 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ known-first-party = [
"openjd",
]

[tool.ruff.lint.per-file-ignores]
# We need to use a platform assertion to short-circuit mypy type checking on non-Windows platforms
# https://mypy.readthedocs.io/en/stable/common_issues.html#python-version-and-system-platform-checks
# This causes imports to come after regular Python statements causing flake8 rule E402 to be flagged
"src/openjd/sessions/_win32/*.py" = ["E402"]


[tool.black]
line-length = 100

Expand All @@ -143,7 +150,9 @@ addopts = [
[tool.coverage.run]
branch = true
parallel = true

plugins = [
"coverage_conditional_plugin"
]

[tool.coverage.paths]
source = [
Expand All @@ -154,6 +163,26 @@ source = [
show_missing = true
fail_under = 80

# https://github.com/wemake-services/coverage-conditional-plugin
[tool.coverage.coverage_conditional_plugin.omit]
"sys_platform != 'win32'" = [
"src/openjd/sessions/_win32/*.py",
"src/openjd/sessions/_scripts/_windows/*.py",
"src/openjd/sessions/_windows*.py"
]

[tool.coverage.coverage_conditional_plugin.rules]
# This cannot be empty otherwise coverage-conditional-plugin crashes with:
# AttributeError: 'NoneType' object has no attribute 'items'
#
# =========== WARNING TO REVIEWERS ============
#
# Any rules added here are ran through Python's
# eval() function so watch for code injection
# attacks.
#
# =========== WARNING TO REVIEWERS ============

[tool.semantic_release]
# Can be removed or set to true once we are v1
major_on_zero = false
Expand Down
1 change: 1 addition & 0 deletions requirements-testing.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
coverage[toml] == 7.*
coverage-conditional-plugin == 0.9.*
pytest == 8.0.*
pytest-cov == 4.1.*
pytest-timeout == 2.2.*
Expand Down
2 changes: 1 addition & 1 deletion src/openjd/sessions/_embedded_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from ._os_checker import is_windows

if is_windows():
from ._win32._helpers import get_process_user
from ._win32._helpers import get_process_user # type: ignore

__all__ = ("EmbeddedFilesScope", "EmbeddedFiles")

Expand Down
145 changes: 0 additions & 145 deletions src/openjd/sessions/_popen_windows_as_user.py

This file was deleted.

Loading

0 comments on commit 72ff65b

Please sign in to comment.