-
Notifications
You must be signed in to change notification settings - Fork 13
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
Conversation
17b8542
to
36a75cf
Compare
src/openjd/sessions/_session_user.py
Outdated
""" | ||
|
||
@staticmethod | ||
def upn_to_down_level(upn): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Missing the type in the function definition. Please also add the docstring for the function. You could find the format by checking other functions.
- 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
- Don't test both branches of the "if upn: convert; else: don't convert" statement. Just test that when provided
user
ordomain\user
, we don't perform any conversion or get an exception from the win32 API. - 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
src/openjd/sessions/_session_user.py
Outdated
upn, win32api.NameUserPrincipal, win32api.NameSamCompatible | ||
) | ||
|
||
def __init__(self, user: str, *, group: str) -> None: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
99d630e
to
b6fc3b2
Compare
Signed-off-by: Robert Luttrell <[email protected]>
b6fc3b2
to
1d8630d
Compare
src/openjd/sessions/_session_user.py
Outdated
user (str): The user. This can be in UPN form, domain\\username, or just username | ||
""" | ||
upn_format_regex = ( | ||
'^(?!\\.)[^\\s\\\\/:*?"<>|]{1,15}\\@[^\\s\\@\\\\/:*?"<>|]+\\.[^\\s\\@\\\\/:*?"<>|]+' |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
src/openjd/sessions/_session_user.py
Outdated
|
||
self.group = group | ||
|
||
self.user = self.try_convert_upn(user) |
There was a problem hiding this comment.
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:
- Ad-joined by using NetGetJoinInformation
@
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...
src/openjd/sessions/_session_user.py
Outdated
import win32security | ||
|
||
import re | ||
|
||
from typing import Optional | ||
|
||
__all__ = ("PosixSessionUser", "SessionUser") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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")
src/openjd/sessions/_session_user.py
Outdated
return re.search(upn_format_regex, user) is not None | ||
|
||
@staticmethod | ||
def try_convert_upn(user): |
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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 | ||
""" |
There was a problem hiding this comment.
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]>
Signed-off-by: Robert Luttrell <[email protected]>
src/openjd/sessions/_session_user.py
Outdated
def __init__(self, user: str, *, group: str) -> None: | ||
""" | ||
Arguments: | ||
user (str): The user |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/openjd/sessions/_session_user.py
Outdated
""" | ||
Arguments: | ||
user (str): The user | ||
group (str): The group |
There was a problem hiding this comment.
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
There was a problem hiding this 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.
Signed-off-by: Robert Luttrell <[email protected]>
Signed-off-by: Robert Luttrell <[email protected]>
src/openjd/sessions/_session_user.py
Outdated
|
||
|
||
class WindowsSessionUser(SessionUser): | ||
__slots__ = ("user", "group") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
Signed-off-by: Robert Luttrell <[email protected]>
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:
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.