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(pedm): netmgmt wrapper to add/remove local users from local groups #1099

Merged
merged 1 commit into from
Nov 11, 2024

Conversation

thenextman
Copy link
Member

For a "MakeMeAdmin" style EDM mode, we require the ability to check if the user is a local administrator, and add or remove them from the local Administrators group.

Add a WinAPI wrapper over the network management functions NetLocalGroupAddMembers, NetLocalGroupGetMembers, and NetLocalGroupGetMembers. This allows us to query the membership of local groups and add or remove users.

I added functions in devolutions_pedm::utils to demonstrate usage, although that will not be final "home" for this logic. We can query if a user is directly a member of the local administrators (in which case, we may add or remove them) or if they are a member of local administrators by virtue of a nested group.

Changelog: ignore

Copy link

github-actions bot commented Nov 8, 2024

Let maintainers know that an action is required on their side

  • Add the label release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) when you request a maintainer to cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

  • Add the label release-blocker Follow-up is required before cutting a new release if a follow-up is required before cutting a new release

  • Add the label publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) when you request a maintainer to publish libraries (Devolutions.Gateway.Utils, OpenAPI clients, etc.)

  • Add the label publish-blocker Follow-up is required before publishing libraries if a follow-up is required before publishing libraries

@thenextman thenextman force-pushed the agent-pedm-add-remove-local-users branch from 5553219 to dc724c7 Compare November 8, 2024 15:45
@thenextman thenextman requested a review from CBenoit November 8, 2024 15:49
@thenextman thenextman marked this pull request as ready for review November 8, 2024 15:49
Copy link
Member

@CBenoit CBenoit left a comment

Choose a reason for hiding this comment

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

The code is absolutely looking good to me. Unsafe code is also properly documented 💯
Since you asked for pedantry, let me be pedantic 😉
I'll approve now so you can merge when you see fit!

crates/devolutions-pedm/src/utils.rs Outdated Show resolved Hide resolved
crates/devolutions-pedm/src/utils.rs Outdated Show resolved Hide resolved
Comment on lines 79 to 105
let mut is_member = is_token_member_of_administrators_group_directly(user_token)?;

if !is_member {
let local_admin_group_sid = Sid::from_well_known(
win_api_wrappers::raw::Win32::Security::WinBuiltinAdministratorsSid,
None,
)?;
let local_admin_group_account = local_admin_group_sid.account(None)?;
let local_admin_sids = get_local_group_members(local_admin_group_account.account_name)?;
let group_sids_and_attributes = user_token.groups()?.0;

for user_group_sid_and_attributes in group_sids_and_attributes {
for admin_sid in &local_admin_sids {
is_member |= admin_sid == &user_group_sid_and_attributes.sid;

if is_member {
break;
}
}

if is_member {
break;
}
}
}

Ok(is_member)
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: I think the code can be made more straightforward and shorter, but there is also personal taste in the mix.

  1. Early exit instead of maintaining a state that we modify along the way.
  2. Less indentation.
  3. Extra if block removed.
if is_token_member_of_administrators_group_directly(user_token)? {
    return Ok(true);
}

let local_admin_group_sid = Sid::from_well_known(
    win_api_wrappers::raw::Win32::Security::WinBuiltinAdministratorsSid,
    None,
)?;
let local_admin_group_account = local_admin_group_sid.account(None)?;
let local_admin_sids = get_local_group_members(local_admin_group_account.account_name)?;
let group_sids_and_attributes = user_token.groups()?.0;

for user_group_sid_and_attributes in group_sids_and_attributes {
    for admin_sid in &local_admin_sids {
        if admin_sid == &user_group_sid_and_attributes.sid {
            return Ok(true);
        }
    }
}

Ok(false)

The second if is_member block seemed not useful to me, as we are already checking just after is_member is modified.

Going further, using iterators you can also turn this

for user_group_sid_and_attributes in group_sids_and_attributes {
    for admin_sid in &local_admin_sids {
        if admin_sid == &user_group_sid_and_attributes.sid {
            return Ok(true);
        }
    }
}

Ok(false)

into this

group_sids_and_attributes
    .flat_map(|local_admin_sids| local_admin_sids.iter())
    .any(|admin_sid| admin_sid == &user_group_sid_and_attributes.sid)

Resulting into this code:

if is_token_member_of_administrators_group_directly(user_token)? {
    return Ok(true);
}

let local_admin_group_sid = Sid::from_well_known(
    win_api_wrappers::raw::Win32::Security::WinBuiltinAdministratorsSid,
    None,
)?;
let local_admin_group_account = local_admin_group_sid.account(None)?;
let local_admin_sids = get_local_group_members(local_admin_group_account.account_name)?;
let group_sids_and_attributes = user_token.groups()?.0;

let is_admin = group_sids_and_attributes
    .flat_map(|local_admin_sids| local_admin_sids.iter())
    .any(|admin_sid| admin_sid == &user_group_sid_and_attributes.sid)

Ok(is_admin)

Which is in my opinion easier to read. Feel free to adjust to your preferences.

(note: Code which does not use iterators may sometimes be easier to read, but as you know there is no easy metric for that 😂 )

Copy link
Member Author

Choose a reason for hiding this comment

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

I did revise this per your recommendations, but I left the iterative solution at the end. In this instance where I think it's purely a stylistic thing, I still find that easier to parse than the declarative approach. I expect my mindset to shift here (in C# I generally prefer the declarative approach, but my comfort levels are different there)

crates/win-api-wrappers/src/netmgmt.rs Outdated Show resolved Hide resolved
crates/win-api-wrappers/src/netmgmt.rs Outdated Show resolved Hide resolved
Comment on lines 26 to 28
// WideString holds a null-terminated UTF-16 string, and as_pwstr() returns a valid pointer to it.
let rc =
unsafe { NetLocalGroupAddMembers(None, group_name.as_pcwstr(), 0, &group_info as *const _ as *const u8, 1) };
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Use the .cast() method for pointer-to-pointer casts (rust-lang/rust-clippy#8017 / https://rust-lang.github.io/rust-clippy/master/index.html#ptr_as_ptr)

Copy link
Member Author

Choose a reason for hiding this comment

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

Left this as-is for now, because I'm not sure I know the best way to follow your suggestion.

Is it the case that I need to break out an intermediate variable?

i.e.

let group_name_ptr = &group_info as *const LOCALGROUP_MEMBERS_INFO_0;

and then use cast() at the final call site, i.e.

unsafe { NetLocalGroupDelMembers(None, group_name.as_pcwstr(), 0, group_name_ptr.cast(), 1) };

But probably I misunderstand

Copy link
Member

Choose a reason for hiding this comment

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

You can wrap with parenthesis:

(&group_info as *const _).cast::<u8>()

But splitting into several lines may be more readable in some cases, especially if we spell out the type like in your example.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks!


let group_name = WideString::from(group_name);

let user_sid = RawSid::try_from(security_identifier)?;
Copy link
Member

Choose a reason for hiding this comment

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

note: Since you use anyhow::Result, you can attach a context string to the error.

For instance:

Suggested change
let user_sid = RawSid::try_from(security_identifier)?;
let user_sid = RawSid::try_from(security_identifier).context("failed to convert security identifier into a RawSid")?;

You need to have the anyhow::Context trait in scope:

use anyhow::Context as _;

This is just an example, it's not necessary to have context for everything either. Depending on the situation you may want to not use anyhow::Result at all.

@thenextman thenextman force-pushed the agent-pedm-add-remove-local-users branch from dc724c7 to 7e902de Compare November 11, 2024 16:59
@thenextman thenextman force-pushed the agent-pedm-add-remove-local-users branch from 7e902de to e7e6676 Compare November 11, 2024 17:13
@thenextman thenextman merged commit 3c3254b into master Nov 11, 2024
30 checks passed
@thenextman thenextman deleted the agent-pedm-add-remove-local-users branch November 11, 2024 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants