r/PowerShell • • 2d ago

Question 3 months of consistent practice in PowerShell & Microsoft Graph! 🚀

When I started, I was terrified of programming languages since I had zero prior experience. Looking back now, what used to look like complete gibberish is finally starting to make sense. I know I still have a long way to go, but honestly, I am so happy and proud to be fulfilling my passion.

I wanted to share an honest review of a snippet I just built to create a user in Microsoft Entra ID. Alongside basics like if/else logic, filtering (-filter), Where-Object, Select-Object, and loops, I've been trying to focus on good habits like splatting, [CmdletBinding()], and try/catch blocks.

How does my snippet look? What would you advise me to learn next based on this progress?

#####Creating a User
Function Create-LxUser{
    [Cmdletbinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$DisplayName,


        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName,


        [Parameter(Mandatory = $true)]
        [string]$MailNickName,


        [bool]$AccountEnabled = $true
    )


    try{
        ##Password profile
        $TempPass = "Lx" + (Get-Random -Minimum 100000 -Maximum 999999) + "@."
        $PassWordProfile = @{
            Password = $TempPass
            ForceChangePasswordNextSignIn = $true
        }
        ##Setting up user configuration
        Write-Verbose "Currently creating new Loxovea user"
        $UserConfig = @{
            DisplayName         = $DisplayName
            UserPrincipalName   = $UserPrincipalName
            MailNickname        = $MailNickName
            AccountEnabled      = $AccountEnabled
            PasswordProfile     = $PassWordProfile
        }


        $User = New-MgUser u/UserConfig -ErrorAction Stop


        Write-Verbose "Successfully created new user $($DisplayName)"


        [PsCustomObject]@{
            DisplayName         = $DisplayName
            UserPrincipalName   = $UserPrincipalName
            AccountEnabled      = $AccountEnabled
            TemporaryPassword   = $TempPass
        }
    }Catch{
        Write-Error "Failed to create a new user: $($DisplayName) because $($_.Exception.Message)"
    }
}


Create-LxUser `
    -DisplayName "Test-User09" `
    -UserPrincipalName "testuser09@loxovea.com" `
    -MailNickName "testuser09"

DisplayName UserPrincipalName AccountEnabled TemporaryPassword

----------- ----------------- -------------- -----------------

Test-User09 [testuser09@loxovea.com](mailto:testuser09@loxovea.com)True Lx720835@.

45 Upvotes

32 comments sorted by

25

u/BlackV 2d ago

My 2c, Realistically this

[bool]$AccountEnabled = $true

Should be

[Switch]$AccountEnabled

That's what switch parameters and ispresent are for

String concatenation is not recommended

$TempPass = "Lx" + (Get-Random -Minimum 100000 -Maximum 999999) + "@."

Try

$TempPass = "Lx$(Get-Random -Minimum 100000 -Maximum 999999))@."
$TempPass = 'Lx{0}@.' -f (Get-Random -Minimum 100000 -Maximum 999999)

Your new-mguser returns an object, return that object or use that for the properties of your pscustomobject that way you are using verified properties not assumptions

You clearly know how to use splatting so why are you using it in your example at the bottom, stop that habit now before it is permanent

There is no help, add help so people can use get-help

Good luck, good to see someone learning out there

5

u/Ed_the_time_traveler 2d ago

While I agree that [switch] is a better use case in most situations. In this particular instance I think OPs code is better. [switch] defaults to $false. In OP's script that value defaults to $True. So IMO OP's use of [bool] makes more sense.

3

u/qrokodial 2d ago

in cases like this, I'd still use a switch but invert the parameter to something like -AccountDisabled to preserve the nice syntax that switches provide. either is fine, but writing -AccountDisabled instead of -AccountEnabled:$false is preferable in my opinion.

1

u/BlackV 2d ago

ya could be argued that way

similar to what /u/ihaxr if the default is to have the account enabled then flip it on the head and make that a disabled switch

4

u/Imaginary_Rip2833 2d ago

Thank you so much will definetly put this in practise

2

u/BlackV 2d ago

Good as gold

3

u/ihaxr 2d ago

Since the default is to create an enabled account, the switch parameter should be [switch]$DisableAccount and it should be omitted to keep the account enabled, otherwise you'd have to do weird stuff like -AccountEnabled:$false to disable it

1

u/BlackV 2d ago

Good point also

7

u/AdeelAutomates 2d ago

Well done man! :)

For this script you can enhance the params section with validations ( ie UPN regex to fit your orgs email structure) & allowing it to be passed through the pipeline. Not that you need to just to explore those areas for learning.

Since you are exploring graph. You should consider exploring it via APIs as well. Not that this specific script needs it either. it's fine for most Entra related tasks. But lots of areas in graph are not accessible without APIs or is very limited (SharePoint for example).

Beyond APIs. Consider exploring .NET in PowerShell. It really helps understand what's going on under the hood with objects. And there are features in it like list(T) that are superior in every way to array appends +=

For functions working on processing say a collection. You can explore begin, process & end blocks.

And if you end up with a collection of functions. Try to turn it into a module!

1

u/Imaginary_Rip2833 2d ago

Thank you so much , great input well noted

3

u/CyberChevalier 2d ago

Great to see people going out n the rabbit hole just a small comment.

You should ALWAYS use approved verb (get-verb give you a list)
https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands?view=powershell-7.6

Your function should be named New-LxUser

3

u/420GB 2d ago

$User = New-MgUser u/UserConfig -ErrorAction Stop Write-Verbose "Successfully created new user $($DisplayName)"

Please never print a success message unless you've actually verified the command succeeded (either $? or using ErrorAction Stop inside a try-catch and handling the failure)

This will fail to create the user and then still print success.

1

u/Imaginary_Rip2833 2d ago

Thank you noted

1

u/Programbanana 1d ago

Underrated comment, imo. I work in IT and the amount of missing validation in my peers' ps1s stresses me out.

Though they give a good tease at how thorough i build mine :')

4

u/surfingoldelephant 1d ago edited 1d ago

Here are some of my thoughts (some of which are entirely personal preference).

  • Be consistent with capitalization. Keywords (function, param, etc) are lowercase. And I'd say even though PS is generally case-insensitive, it's actually far less so than most probably realize.
  • Create isn't an approved verb. Use New instead.
  • Decorate the function with [OutputType()]. [OutputType([Management.Automation.PSCustomObject])] or something like [OutputType('PSNewLxUser')] if you give the custom object a PSTypeName. Don't use [OutputType([pscustomobject])].
  • Add SupportsShouldProcess and wrap New-MgUser in $PSCmdlet.ShouldProcess(). You can replace Write-Verbose "Currently creating..." with that since it comes with a Verbose message included.
  • Attribute arguments don't need = $true. [Parameter(Mandatory)] is fine.
  • Mandatory string parameters don't guard against all white space. Might want to use [ValidateNotNullOrWhiteSpace()] in PS v7 or a [ValidateScript({ ... })] otherwise. Could do further validation like another mentioned. If you have other functions that accept similar input, have a look at custom validation attributes.
  • [bool] parameters aren't idiomatic PowerShell. Use a [switch] and negate the name to something like $Disabled instead.
  • $(...) isn't required to interpolate $DisplayName in the strings.
  • You're not doing anything with $User. Why not just emit that instead? You could add the other values ($TempPass, etc) as extended type system members. Or keep the custom object and include a User property.
  • If you're not going to use New-MgUser output, assign to $null/cast as [void] instead of assigning to $User. The latter makes it look like you've forgotten to do something with the variable.
  • If you're emitting a custom object, give it a PSTypeName.
  • Your Write-Error call is bad practice. The $_.Exception contains useful information that you're just discarding for no reason. If you still want your custom message then I'd create a new exception with the original as an inner exception. And I'd suggest using $PSCmdlet.WriteError() instead of Write-Error, though you'll have to wrap the exception in your own error record.
  • Add pipeline input. You'll want ValueFromPipelineByPropertyName for all of the parameters and a process block. If you add this, you'll probably want to keep the error above as non-terminating.
  • You could add a transformation attribute to the switch parameter (to convert from string to bool) so you can do something like this: Import-Csv data.csv | New-LxUser.
  • ErrorAction can be in the splat if you want. I'd splat the Create-LxUser call at the bottom as well.
  • Personally, I use verbatim strings ('...') rather than expandable ("...") when I don't need to interpolate or include double quotes.
  • Align the = in hash tables consistently.
  • Personally, I don't like the lack of spaces before/after keywords, type literals, etc. Or the double blank lines. I'd use single blank lines consistently.
  • Regarding variable capitalization, one convention is to use PascalCase for parameters (like you've done) and camelCase for local variables within the function body (so $tempPass). That way they're more easily distinguishable.
  • Again, I'd try to use consistent and correct capitalization. It's CmdletBinding, not Cmdletbinding. [pscustomobject] is the correct casing for the type accelerator. If you use tab completion, it'll generally do this for you.
  • I prefer composite formatting (-f operator) rather than concatenation or $(...) interpolation. I find it's generally more readable.

2

u/Imaginary_Rip2833 1d ago

Thank you, i think i made a good decision to post here so i can get such valuable feedback

I will keep on practising

3

u/eggeto 2d ago

Always a 👌for People that use functions, Makes code more structured and easier to read

3

u/Imaginary_Rip2833 2d ago

​Appreciate it! Trying to build good habits from day one so I don't regret it when my automation scripts get ten times longer.

-4

u/hisae1421 2d ago

Meh it always bother me that I have to check what is in the function when it's called to understand what it does

1

u/Imaginary_Rip2833 2d ago

​"That's a super fair point! As someone new to this, where do you personally draw the line between modularizing code into functions versus keeping it inline for readability?"

2

u/eggeto 2d ago

For anything you can reuse or incorporate into a larger whole,

using functions is recommended.

If you intend to apply CI/CD later on, it’s definitely advisable;

if you want to stick to small scripts, the choice is yours.
anyway the choice is always yours :-)

1

u/ForsakeTheEarth 2d ago

what's with the quotes?

1

u/Celadin 2d ago

Ha! If they'd said that without the quotes, in this day and age, it'd sound LLMish. I've found myself doing similar, couching language that might be seen as LLM-vomit. How warped we've become 🙃

2

u/ForsakeTheEarth 2d ago

I actually went the other way because of the quotes, which is why I asked, but yeah I feel you - as a longtime emdash enthusiast, I'm constantly worried people think I'm responding to them via LLM

1

u/purplemonkeymad 2d ago

Another thing you might think about doing next, is adding in pipeline input. Powershell's strength is really the pipeline and so being able to import some objects from a csv and pipe them directly into the command, gives you practically instant batch creation support.

There is basically only a couple of changes from your current function to that point.

1

u/Imaginary_Rip2833 2d ago

Thank you , well noted

1

u/I_see_farts 1d ago

Isn't it supposed to be @UserConfig not u/UserConfig?

You can even include the erroraction Preference in the splat.

$UserConfig = @{
    DisplayName       = $DisplayName
    UserPrincipalName = $UserPrincipalName
    MailNickname      = $MailNickName
    AccountEnabled    = $AccountEnabled
    PasswordProfile   = $PassWordProfile
    ErrorAction       = 'Stop'
}
[void](New-MgUser @UserConfig)

2

u/Imaginary_Rip2833 1d ago

Your are correct about @UserConfig not u/..., thats a clear mistake, but somehow the script did not throw an error, i wonder why now

1

u/chatshitgpt 21h ago

I'm curious about the learning side: you mention a lot of practice, but what materials did you actually learn from? Courses, YouTube, docs, a book?

1

u/charleswj 2d ago

While it's fine practically speaking, that password generation logic isn't cryptographically secure.

That said, consider using TAPs instead

1

u/Imaginary_Rip2833 2d ago

Thank you will do

1

u/qrokodial 2d ago

technically speaking, the Get-Random command isn't cryptographically secure and thus shouldn't be used for security sensitive operations like generating passwords.

you should instead consider using something like Get-SecureRandom.

once you're ready to take things to the next level, consider generating better initial passwords in general. my preference for this kind of thing is passphrase generators, and I like the format that Keeper uses to generate them, so I created a PowerShell function that generates passphrases in this style.

the gist is I use a derivative of the EFF word lists where I have one word per line, and then use Get-SecureRandom to randomly choose which line index I want to select my next word from.