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

feat: Add Windows session user #16

Merged
merged 7 commits into from
Oct 12, 2023
Merged

Conversation

roblutt
Copy link
Contributor

@roblutt roblutt commented Oct 4, 2023

What was the problem/requirement? (What/Why)

To support impersonation on Windows, we need a Windows session user class

What was the solution? (How)

Create session user class with the following properties:

  • User (local username, domain user's UPN, or domain user in down-level logon form)
  • Group (local group name or domain group in down-level)

Convert user to down-level because we cannot initiate a powershell subprocess using the domain user's UPN. This conversion uses the pywin32 package, which is a wrapper around Microsoft's win32 dlls. In particular, we use translatename, which calls TranslateNameW under the hood. Description of the enum values for the input and desired name formats can be found here.

What is the impact of this change?

User impersonation on Windows can be implemented

How was this change tested?

Unit tests pass. This is not yet covered by unit tests, but it was tested manually with a Windows machine joined to an AD.

Was this change documented?

No

Is this a breaking change?

No


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@roblutt roblutt force-pushed the roblutt/windows-session-user branch 2 times, most recently from 17b8542 to 36a75cf Compare October 4, 2023 02:13
@roblutt roblutt changed the title Add Windows session user feat: Add Windows session user Oct 4, 2023
@roblutt roblutt marked this pull request as ready for review October 4, 2023 02:21
@roblutt roblutt requested a review from a team as a code owner October 4, 2023 02:21
"""

@staticmethod
def upn_to_down_level(upn):
Copy link
Contributor

@Honglichenn Honglichenn Oct 4, 2023

Choose a reason for hiding this comment

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

  1. Missing the type in the function definition. Please also add the docstring for the function. You could find the format by checking other functions.
  2. I wonder if the input string is not the UPN format, will this fail? For example, Admin instead of [email protected]? I guess it will? We should check the format before translate it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Better to have some basic unit tests in this PR, so we are more confident that this can work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, this may fail if provided Admin or even domain\\Admin, depending on how the win32 API handles this. I'll add a check and some unit tests

Copy link
Contributor Author

@roblutt roblutt Oct 4, 2023

Choose a reason for hiding this comment

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

In terms of testing, we have some difficulty here because the translatename call is not doing a simple string conversion, but contacting the AD DC. So we can't test the translatename call yet as this is dependent on being joined to an AD and having the users in the test configured. After adding some logic to validate that we're in valid UPN format, I see 2 options:

  1. Don't test both branches of the "if upn: convert; else: don't convert" statement. Just test that when provided user or domain\user, we don't perform any conversion or get an exception from the win32 API.
  2. Refactor the windows session user class to inject a converter as a dependency so we can mock it out. Then we can assert on it being called and not called in cases of UPN and not UPN. This adds more complexity than it's worth, so let's avoid this for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

we have some difficulty here because the translatename call is not doing a simple string conversion, but contacting the AD DC

To be clear, are you saying that this code path requires that the host be domain joined? Or will this work with a locally configured user as well?

Also, to second Hongli, a docstring here would be fantastic. Comments as well -- I'd like to better understand why we need to be doing this.

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've updated the code since this comment. The code path that performs the conversion does require that the instance is joined to whichever domain is specified in the UPN, but if anything other than a UPN is provided, the instance does not need to be joined to the domain for these methods.

Added docstrings

upn, win32api.NameUserPrincipal, win32api.NameSamCompatible
)

def __init__(self, user: str, *, group: str) -> None:
Copy link

Choose a reason for hiding this comment

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

Question for @roblutt and @ddneilson - Do we add input validations for the session user class? For instance we have defined a regex pattern for user and group for posix on our SDK model, should a similar thing exist here? Similarly, should we add the valid formats for user and group in windows?

I haven't previously ever developed on open source modules. So looking for guidance here too.

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 think the changes here are in line with what we have on Linux. Curious to hear what @ddneilson has to say though

Copy link
Contributor

Choose a reason for hiding this comment

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

Good thinking, @anandgo1 , but I think that we're okay without a regex here. Let's just make sure that our uses of the username/groupname are properly escaped when using them in powershell/batch scripts.

@roblutt roblutt force-pushed the roblutt/windows-session-user branch from 99d630e to b6fc3b2 Compare October 5, 2023 00:02
Signed-off-by: Robert Luttrell <[email protected]>
@roblutt roblutt force-pushed the roblutt/windows-session-user branch from b6fc3b2 to 1d8630d Compare October 5, 2023 00:08
user (str): The user. This can be in UPN form, domain\\username, or just username
"""
upn_format_regex = (
'^(?!\\.)[^\\s\\\\/:*?"<>|]{1,15}\\@[^\\s\\@\\\\/:*?"<>|]+\\.[^\\s\\@\\\\/:*?"<>|]+'
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind sharing how you write this regex? I am worried if we miss anything here, [^\\s\\\\/:*?"<>|]{1,15} it seems we limit the length of the user name is less than 15, and I have a feeling that it may be not true in all systems. For example, this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like the 15 is the wrong limit, it should be 256 per this doc.

Invalid characters in the suffix come from here.

^(?!\\.): negative lookahead to assert we don't start with a period

[^\\s\\\\/:*?"<>|]{1,15}: 1-15 characters which cannot be in this list. I'll update this to 256 max

\\@: @

[^\\s\\@\\\\/:*?"<>|]+: 1 or more characters which cannot be in the list

\\.: .

[^\\s\\@\\\\/:*?"<>|]+ 1 or more characters which cannot be in the list


self.group = group

self.user = self.try_convert_upn(user)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it simpler that we just check if the machine is:

  1. Ad-joined by using NetGetJoinInformation
  2. @ is in the username. Based on this, local user is not allowed to use @.

Then convert it? I am not sure if we should use a strict regex here, since user can always create some username too creative. We saw it happened several times in Nimble.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That could be simpler than what I've implemented. Are you suggesting that we use the name returned by NetGetJoinInformation and compare to the UPN suffix to ensure that we're joined to the domain in the suffix before doing the conversion, or just use the int returned by NetGetJoinInformation to ensure that we're joined to any domain before converting?

Copy link
Contributor

Choose a reason for hiding this comment

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

Are you considering this edge case that we discussed in the slack? If I understand correctly, you mean NetGetJoinInformation can also handle this edge case? It is a good point.
But I think we may need to consider the AD trust use case. there maybe some cross-domain user. I am not sure how does it work here.
So maybe just do a simple one to just use the int returned by NetGetJoinInformation to ensure that we're joined to any domain before converting and write a comment for the edge case which we don't support. I am open to any other opinions, since I am not an expert for the AD...

import win32security

import re

from typing import Optional

__all__ = ("PosixSessionUser", "SessionUser")
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: maybe you want to update this too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By update, do you mean move re inside the condition to only import if we're on Windows? I guess this is a moot point either way based on the recommendation to just check the string for a "@" instead of using the regex, but just curious.

Copy link
Contributor

Choose a reason for hiding this comment

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

hmmm.. I mean L17 __all__ = ("PosixSessionUser", "SessionUser") -> __all__ = ("PosixSessionUser", "SessionUser", "WindowsSessionUser")

return re.search(upn_format_regex, user) is not None

@staticmethod
def try_convert_upn(user):
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: I think the function name is a little bit ambiguous. Without reading the function's documentation or implementation, a developer might not be sure if the function is converting to UPN or from UPN. Maybe convert_upn_to_downlevel?

It is better to add the typing. For example, def convert_upn_to_downlevel(user:str)->str:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will change the name next revision

Otherwise, returns user unmodified.
Arguments:
user (str): The user. This can be in UPN form, down-level logon, or just username
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

For the docstring, I think we should also include doc for the return value as well. I am not quite sure which standard we are using in the github packages. Need some clarification from @ddneilson. I see different formats of docstring in different files.

Signed-off-by: Robert Luttrell <[email protected]>
Signed-off-by: Robert Luttrell <[email protected]>
def __init__(self, user: str, *, group: str) -> None:
"""
Arguments:
user (str): The user
Copy link

Choose a reason for hiding this comment

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

More specifically, lets call this Windows session User

Copy link
Contributor

Choose a reason for hiding this comment

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

It is good to specific, but I believe we will move these two common attributes to the SessionUser later based on previous discussion.

"""
Arguments:
user (str): The user
group (str): The group
Copy link

Choose a reason for hiding this comment

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

Let's call this Windows session Group

src/openjd/sessions/_session_user.py Show resolved Hide resolved
Honglichenn
Honglichenn previously approved these changes Oct 6, 2023
anandgo1
anandgo1 previously approved these changes Oct 6, 2023
Copy link
Contributor

@ddneilson ddneilson left a comment

Choose a reason for hiding this comment

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

There's some great discussion on this one! I just have one little nitpick that I'd like to see in the __init__ docstring.

src/openjd/sessions/_session_user.py Show resolved Hide resolved
@roblutt roblutt dismissed stale reviews from anandgo1 and Honglichenn via fb07f18 October 10, 2023 20:42
Honglichenn
Honglichenn previously approved these changes Oct 11, 2023
Honglichenn
Honglichenn previously approved these changes Oct 11, 2023
src/openjd/sessions/_session_user.py Show resolved Hide resolved


class WindowsSessionUser(SessionUser):
__slots__ = ("user", "group")

Choose a reason for hiding this comment

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

Should we add "password" here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes

@ddneilson ddneilson merged commit 4e954e6 into mainline Oct 12, 2023
8 checks passed
@ddneilson ddneilson deleted the roblutt/windows-session-user branch October 12, 2023 16:54
@ddneilson ddneilson added the security Pull requests that could impact security label Jan 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
security Pull requests that could impact security
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants