Automating box processes using powershell and the box windows sdk
Hey all. I thought I would (re)post a quick explanation as to how you can use the box windows sdk in powershell to automate box tasks. This method will require powershell 5 at least (or if you separately installed powershellget) to work. First, obtaining the assemblies. I created a custom little nuget package of my own which contains not only the box assembly needed to automate box tasks, but the dependencies that it requires as well. I did this due to, among other things, the errors encountered when using nuget to obtain the assemblies and their dependencies and trying to import them into powershell, and also because of the number of unnecessary assemblies that this method imports as well. I published the package on MyGet (which you can download here if you want to see the contents) to make it simple for a powershell script to download these assemblies to the local machine in the event that the powershell script could not find them. Below is the code powershell code that I use to accomplish this (using the packagemanagement module):
Get-PackageProvider -Name NuGet -ForceBootstrap Set-PSRepository -Name PSGallery -InstallationPolicy Trusted If (!(Test-Path "$env:ProgramData\boxlib")) { New-item -Name "boxlib" -Path $env:PROGRAMDATA -ItemType Directory -Force } If (!(Test-Path "$env:ProgramData\boxlib\Box.Dependencies\1.0.0\lib\net45\Box.V2.dll")) { Get-PackageProvider -Name NuGet -ForceBootstrap Get-PackageProvider -Name Powershellget -ForceBootstrap Register-PackageSource -Name "Box.Dependencies" -Location "https://www.myget.org/F/boxdependency/api/v2" -ProviderName "PowershellGet" -Trusted -Force save-Package -Name Box.dependencies -path "$env:ProgramData\boxlib" -ProviderName "PowershellGet" } Get-ChildItem "$env:ProgramData\boxlib\*.dll" -Recurse | % { [reflection.assembly]::LoadFrom($_.FullName) }
This will successfully import all of the box windows sdk assemblies you will need into the current powershell session.
While figuring out how to successfully import the box windows sdk was a challenge in of itself, figuring out how to use the box windows sdk was almost just as much a challenge. So lets talk about authenticating to box via a JWT assertion. This part was actually relatively simple, as this procedure is covered on the box windows sdk github page (the below code assumes that you generated your RSA keypair in the developement console, as it contains all of the information needed to authenticate using JWT):
$cli = Get-Content "Path\to\json\authentication\file\box-cli.json" | ConvertFrom-Json $boxconfig = New-Object -TypeName Box.V2.Config.Boxconfig($cli.boxAppSettings.clientID, $cli.boxAppSettings.clientSecret, $cli.enterpriseID, $cli.boxAppSettings.appAuth.privateKey, $cli.boxAppSettings.appAuth.passphrase, $cli.boxAppSettings.appAuth.publicKeyID) $boxJWT = New-Object -TypeName Box.V2.JWTAuth.BoxJWTAuth($boxconfig) $boxjwt $tokenreal = $boxJWT.AdminToken() $adminclient = $boxJWT.AdminClient($tokenreal) $adminclient
NOTE: If you want to run the box commands under the context of a particular user (like if you wanted to search through a users files/folders, create share links to aforementioned files/folders, etc.), you can specify the user id of this user as the second argument in the creation of the $adminclient object (eg. $adminclient = $boxJWT.AdminClient($tokenreal, $userID)). This is essentially the same as using the "as-user" header. Another quick explanation. The "$adminclient" object is what you will be using to actually perform administrative tasks in box. Lets take a look at an example script that I wrote real quick, shall we? The below script is a script I was instructed to write in order to automate our "user termination process" as far as box is concerned:
<# .SYNOPSIS Obtain all Legal hold objects in the enterprise .DESCRIPTION Obtain a list of all legal hold objects in the enterprise. .EXAMPLE PS C:\> Get-LegalHold .NOTES Additional information about the function. #> function Get-LegalHold { $legalurl = $adminclient2.LegalHoldPoliciesManager.GetListLegalHoldPoliciesAsync() $legalurl.Wait() $legalOutput = $legalurl.Result return $legalOutput } <# .SYNOPSIS Returns all legal hold assignment objects for all enterprise legal holds .DESCRIPTION This function will return all of the legal hold assignment objects, which are basically user objects associated with a particular legal hold, for all of the legal hold objects returned by Get-legalhold function .EXAMPLE PS C:\> Get-LegalHoldAssignments .NOTES Additional information about the function. #> function Get-LegalHoldAssignments { $tyy = Get-legalhold $holds = $tyy.entries $legids = New-Object System.Collections.ArrayList foreach ($hold in $holds) { $newhold = $adminclient2.LegalHoldPoliciesManager.GetAssignmentsAsync($hold.Id) foreach ($nope in $newhold.Result.Entries) { $legids.Add($nope.Id) } } Return $legids } $user = Get-ADUser "ADuser" -Properties Mail, Manager $email = $user.Mail #retrieve information of the manager of the AD user $manager = Get-aduser $user.Manager -Properties Mail, GivenName $manmail = $manager.Mail #Obtain a list of all enterprise box users, loop through the list, and if the user has a box account (identified using user's email), then save the user's and his/her manager's necessary information to variables. $val = 0 $entries = New-Object System.Collections.ArrayList $grog = $adminclient.UsersManager.GetEnterpriseUsersAsync($null, 0, 1000) $grog.Wait() $grog.Result.Entries | % { $entries.Add($_) } $tot = $grog.Result.TotalCount Do { $val = $val + 1000 $temp = $adminclient.UsersManager.GetEnterpriseUsersAsync($null, $val, 1000) $temp.Wait() $temp.Result.Entries | % { $entries.Add($_) } } while ($val + 1000 -lt $tot) $id = $null $name = $null $login = $null $manid = $null $manlogin = $null Foreach ($entry in $entries) { If ($entry.login -like $email) { $statusbar1.Text = "$($textbox1.Text) has a box account. Scan will continue. Please wait." $id = $entry.Id $login = $entry.login $name = $entry.Name } If (($manager -ne $null) -and ($manager -ne '')) { If ($entry.login -like $manager.Mail) { $manid = $entry.Id $manlogin = $entry.login } } } #The below code runs if the user has a box account If ($id -ne $null) { #The below code will obtain a list of all legal hold assignments for all legal hold objects to determine if the user is on legal hold. $leghols = Get-LegalHoldAssignments $onlegalhold = $false foreach ($leghol in $leghols) { $newoutput = $adminclient2.LegalHoldPoliciesManager.GetAssignmentAsync($leghol) If ($newoutput.Result.Assignedto.Id -like $id) { $onlegalhold = $true Break } } $holdtext = '' #Below code runs if the user is on legal hold. If ($onlegalhold -eq $true) { #Moves all of the user's box content to the root of the Main "storage" account. $foldproc = $adminclient.UsersManager.MoveUserFolderAsync("$id", "***number removed for privacy***", "0", $false) $foldproc.Wait() $foldid = $null #Searches all of the folders in the Main "storage" account for the folder which was just created containing the user's files. $folddd = $adminclient.FoldersManager.GetfolderItemsAsync("0", 1000, 0, $null) $folddd.Wait() $foldentries = $folddd.result.entries foreach ($foldentry in $foldentries) { If ($foldentry.name -like "*$login*") { $foldid = $foldentry.id Break } Else { Continue } } #When the folder containing the user's files is located, the folder will be moved into the "Legal Holds" folder of the "storage" account. $folderrequest = @{ id = "$foldid" parent = @{ id = "2***phone number removed for privacy***" } } $folderreqproc = $adminclient.FoldersManager.CopyAsync($folderrequest, $null) $folderreqproc.Wait() $folderdelete = $adminclient.FoldersManager.DeleteAsync("$foldid", $true, $null) $folderdelete.Wait() } #Below Code runs if the user is not on legal hold Else { #Below Code runs if the user did not have a manager configured in Active Directory or if the user's manager does not have a box account. If ($manid -eq $null) { $foldproc = $adminclient.UsersManager.MoveUserFolderAsync("$id", "***number removed for privacy***", "0", $false) $foldproc.Wait() #Below code runs if user has a manager who does not have a box account. A shared link to user's box files will be created for manager. If ($manmail -ne $null) { $foldid = $null #Searches all of the folders in the Main "storage" account for the folder which was just created containing the user's files. $folddd = $adminclient.FoldersManager.GetfolderItemsAsync("0", 1000, 0, $null) $folddd.Wait() $foldentries = $folddd.result.entries foreach ($foldentry in $foldentries) { If ($foldentry.name -like "*$login*") { $foldid = $foldentry.id Break } Else { Continue } } #creates the shared link to the folder containing the user's box files. $sharejson = @{ access = "open" } $sharelink = $adminclient.FoldersManager.CreateSharedLinkAsync("$foldid", "$sharejson", $null) $sharelink.Wait() $link = $sharelink.Result.SharedLink.Url } } #Below Code runs if the user's manager does have a box account. User's files will be moved to manager's box account. Else { $foldproc = $adminclient.UsersManager.MoveUserFolderAsync("$id", "$manid", "0", $false) $foldproc.Wait() } } #Deletes user's now empty box account and recovers license. $deleteuser = $adminclient.UsersManager.DeleteEnterpriseUserAsync("$id", $false, $true) $deleteuser.Wait() }
I did my best to annotate the script and be as clear as I could about each step in the process. As you can see (if you followed the script), the script takes an active directory user and that user's manager, loops through all enterprise users to see if they have box accounts (using their ad object's "mail" attribute). If the terminated user has a box account, the script will then determine if that user is on legal hold by cycling through all legal hold assignment of all of the legal hold object in the enterprise. If the user is on legal hold, the users files are stored in a secure folder. If they are not, then we look and see if the user had a configured manager. If they don't, the user's files are move to the same "secure" folder. If they do have a manager, but the manager does not have a box account, a shared link will be created for the manager to use to access that user's box files. If the manager does have a box account, then the files are simply moved to his/her account. There are a couple of things that I want to point out about the script that I feel deserve special mention:
1. You will notice that (almost) everytime I use $adminclient to execute an administrative action, I assign the process to a variable and on the very next line use that variable to call the "Wait()" method. This is because, as you can see from the names of the different box methods, that the box tasks run Asynchronously, and the "wait" method essentially performs the same task as the "await" keyword in C#, in that you wait for the previous command to finish executing and return the results before continuing to process the script. For example, say you have a box user you want to delete, but not force delete, and you use the "$adminclient.UsersManager.MoveUserFolderAsync" method to move this user's content to another box user. However, on the very next line, instead of using the "wait" method to wait for the file transfer process to finish, you use the "$adminclient.UsersManager.DeleteEnterpriseUserAsync" method (where the "force" parameter is set to $false) to attempt to delete the user. You will get an error because the previous action to move the user's files to another account is still processing and has not completed yet. I make it a point to use the "wait" method every time I perform a task using the $adminclient to make sure that all processes and results are returned before the script continues to execute.
2. When I used $adminclient.FoldersManager.CopyAsync($folderrequest, $null) to copy a folder in the script above, some of you may have been slightly confused as to why I assigned the value I did to "$folderrequest." This is because the syntax used is the powershell equivalent of using hashtables in a json object. This link is where I obtained the information I needed to create these variables. As for what information actually needs to be in the variables, look no further than the API documentation page. If you look at the Curl representation of the command, you will find the Json variables which are needed in order to construct the request you need to perform the operation.
I hope this helped some of you. I will be honest, I will probably never check this posting again just because of time constraints, but I hope this put you on the right path to getting the answers you needed to automate box in powershell. Peace.
-
Oh, and one more thing. I also used ILSpy to decompile the box windows sdk dll file, which was infinitely helpful in figuring out what all of usable commands are and what their arguments are.
-
I wanted to add one more piece of information. Thanks to the help of mattwiller on Github, I now finally understand why, when running the windows sdk in powershell, 2 different versions of the Newtonsoft.Json assembly were required (version 9.0.0 and 10.0.3):
It looks like this works in full .NET applications but not in PowerShell because the SDK's app.config file that binds any version of Newtonsoft.Json to the 10.x version that the SDK installs is not being loaded by PowerShell. Fundamentally, since you're going outside of the SDK's normal operation and loading the assemblies manually, you're going to have to ensure that the correct versions are installed and referenced. In the course of our investigation we found some information about how to make PowerShell load the app.config, but weren't able to make it work in our environment.
The app.config file that mattwiller references is located here, and it is a bit difficult to implement if you are running a straight powershell script. Allow me to explain: when you run an executable, if there is a ".config" file in the same directory as the executable and has the same name (eg. "aprocess.exe" would have a config file named "aprocess.exe.config"), the executable (from my understanding) is able to pull settings from this config file for use when the script is run. I did some research into the link that mattwiller posted, but was not able to successfully load the app.config file into the powershell script. Then, it struck me: the answer was not to load an app.config file into the powershell session, as powershell is itself, a process, and has its own config file located in the same directory that the powershell executable is located (C:\Windows\System32\WindowsPowerShell\v1.0). So, due to the fact that the powershell executable is run whenever a script is run on the machine, one would have to modify the powershell.exe.config (and/or powershellise.exe.config) files and add the content of the app.config file linked above in order to get powershell to correctly load the Newtonsoft.json.10.0.3 assembly. If, however, you are like me and don't want to necessarily modify system files like that, good news is that that is not the only option. The second option, however, involves converting the powershell script into an executable file and then creating a config file for that executable. If you use Sapien powershell studio, when you build a package, by default, the config file is created along with the executable, so all you would have to do is add the contents of the app.config file linked above to the newly created config file for the executable powershell studio generated, and presto, works like a charm. Below is what an example config file would look like:
For those who do not have powershell studio and is outside your price range, don't worry, you can still perform this process, although you will have to do it all manually. There is a cool little application called ps1 to exe converter (both a portable and web based) which will convert a powershell script into an executable, and includes other really cool features, like embedding files, choosing the working directory, etc. You can use this tool to convert the powershell script to an executable, then create the config file for the executable, modify it with the contents of the app.config file linked above, (optionally) embed it within your converted executable, and you are good to go.
-
I'm getting the following error when attempting to get the AdminToken. And suggestions on why?
The value of $boxJWT is :
Box.V2.JWTAuth.BoxJWTAuth
$tokenreal = $boxJWT.AdminToken()
Exception calling "AdminToken" with "0" argument(s): "The type initializer for
'System.IdentityModel.Tokens.Jwt.JsonExtensions' threw an exception."
At line:1 char:1
+ $tokenreal = $boxJWT.AdminToken()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : TypeInitializationException -
This is likely because the exact issue that describes in his post above — the Box SDK depends on Newtonsoft.Json 10 but the System.IdentityModel.Tokens.Jwt library we use depends on 9. A full .NET project respects the SDK's app.config file that does a binding redirect to reconcile these dependencies, but if you're loading assemblies manually in PowerShell you're probably not picking that config up and the IdentityModel library is not able to resolve it's dependency on Newtonsoft.Json 9. In the post above, gives some helpful information to fix that issue — could you try that out and see if it works for you?
-
To piggy back off what said, you might find the below code snippet very useful, as it allows you to perform powershell binding redirection:
# Load your target version of the assembly $newtonsoft = [System.Reflection.Assembly]::LoadFrom("$PSScriptRoot\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll") $onAssemblyResolveEventHandler = [System.ResolveEventHandler] { param($sender, $e) # You can make this condition more or less version specific as suits your requirements if ($e.Name.StartsWith("Newtonsoft.Json")) { return $newtonsoft } foreach($assembly in [System.AppDomain]::CurrentDomain.GetAssemblies()) { if ($assembly.FullName -eq $e.Name) { return $assembly } } return $null } [System.AppDomain]::CurrentDomain.add_AssemblyResolve($onAssemblyResolveEventHandler) # Rest of your script.... # Detach the event handler (not detaching can lead to stack overflow issues when closing PS) [System.AppDomain]::CurrentDomain.remove_AssemblyResolve($onAssemblyResolveEventHandler)
-
Hi, I want to know whether we can automate file access process on the box from this code or not?
I am in an internship where I have to automate some process which includes a step where we have to give access of some files to selected users. So, can I use this code to automate file access process? If not then, please tell me any other possible way
-
For those of you who are interested, I have created a full powershell module which acts as a wrapper for the box windows sdk, removing the need for you to directly work with the sdk. To install the module, make sure you have powershellget installed and run
Install-module Poshbox -force -allowclobber
You can view the source code for the module here. You can also submit any issues that you encounter or contribute yourself.
-
Poshbox powershell module can be found here . You can also install poshbox using powershellget:
Install-module poshbox
Please sign in to leave a comment.
Comments
10 comments