Friday, October 28, 2022

Notes About PowerShell: Adding a TreeView to the GUI, Part 1

Previously we built the Simplest PowerShell GUI, and then added a few features to it here.

The script below is that last more complex GUI (with a few edits). It presents a window frame, with an OK button object, and a textbox object that asks for a name to be entered. We'll build on this script to add a treeview object. A treeview allows you to browse a tree-like structure, such as the file system, or an Active Directory domain, or the Windows Registry. So fire up the ol' PowerShell ISE, copy and paste in the following code (making sure the quotation marks don't turn into "smart" quotation marks), and then save the file as something like "tinker.ps1". Then try running it to make sure it works.

# Initialize the PowerShell GUI
Add-Type -AssemblyName System.Windows.Forms
  
# Create a new window form.
$Win = New-Object System.Windows.Forms.Form
$Win.ClientSize = '490,460'                              # Size of window frame.
$Win.text = "My Parent Window"                           # Title of the Win frame.
$Win.BackColor = "#ffffff"                               # Background color of the Win frame.

# Create an OK button.
$buttonOK = New-Object 'System.Windows.Forms.Button'     # Create the button.
$buttonOK.Text = "OK"                                    # Puts text on the button.
$buttonOK.Location = '350, 240'
$buttonOK.Size = '60, 25'
$buttonOK.Anchor = 'Bottom, Right'                       # Keep button in relative bottom-right of Win.
$buttonOK.DialogResult = "OK"                            # "None|OK|Cancel|Abort|Retry|Ignore|Yes|No".
$Win.Controls.Add($buttonOK)                             # Add the button to the form.
$Win.AcceptButton = $buttonOK                            # Click = "Close form; I accept it as it now is."

# A textbox.
$NameBox = New-Object "System.Windows.Forms.Textbox"
$NameBox.Size = "175,25"
$NameBox.Location = "290,40"
$NameBox.Text = "Babushka"                               # Pre-fill the box.
$Win.Controls.Add($NameBox)
$Win.ActiveControl = $NameBox                            # Once form shows, select this box.
  # Put a label with the box.
$Label_NameBox = New-Object 'System.Windows.Forms.Label'
$Label_NameBox.Text = "Enter your name:"
$Label_NameBox.Size = "150, 25"
$Label_NameBox.Location = '290, 20'
$Win.Controls.Add($Label_NameBox)

# Display the form.
$Win.ShowDialog() 

# The computer is now looping forever waiting for user input.
# Once the input is the OK button or the Window's X (or
# keyboard equivalent), the window closes and the code below runs.
If ($Win.DialogResult -eq "OK") {
    Write-Host("The OK button was pressed. The name in the box is `"$($NameBox.Text)`".")
} elseif ($Win.DialogResult -eq "Cancel") {
    Write-Host("The X was pressed. I`'m not going to tell you the name that is in the box.")
} # end of If

Now we're ready to create the empty treeview object. The text in bold below is what we're adding.

   ...
  # Put a label with the box.
$Label_NameBox = New-Object 'System.Windows.Forms.Label'
$Label_NameBox.Text = "Enter your name:"
$Label_NameBox.Size = "175, 23"
$Label_NameBox.Location = '10, 20'
$Win.Controls.Add($Label_NameBox)

# The TreeView object.
$TreeView = New-Object System.Windows.Forms.TreeView
$TreeView.Location = "10,40"
$TreeView.Size = "250,400"
$Win.Controls.Add($TreeView)                             # Add the tree view to the main window form.
  # Put a label with the object.
$Label_TreeView = New-Object System.Windows.Forms.Label
$Label_TreeView.Location = "10,20"
$Label_TreeView.Size = "150,25"
$Label_TreeView.Text = "My Tree Object:"
$Win.Controls.Add($Label_TreeView)                       # Add the label to the form.

# Display the form.
$Win.ShowDialog()
   ...

That's a lot of code to be scrolling up and down in, and it's pretty easy to get lost in it. It'd be good if we could break up this big monolithic piece of code into smaller, more manageable chunks.

Most of this code is involved in the actual GUI-building, so the breaking up into chunks won't do a great deal for us at this point, but it will help to compartmentalize the different functional parts. We'll break this code into two pieces, and put them in separate files. On the one hand, that's a disadvantage, because now you're dealing with multiple files for your project, but on the other hand, it's an advantage, in that it breaks up the project into more manageable bite-size pieces.

So click on Edit / Select All in your PowerShell ISE, then Edit / Copy, to copy all the text into your Clipboard. Now click on click on File / New to open up a new blank tab, and then Edit / Paste to paste the script into a new tab. Go to the very bottom of the file and select everything after the "$Win.ShowDialog()" line, and then Edit / Cut that selection into the Clipboard. Now save this tab as something like "tinker_GUI.ps1".

You now have two almost identical files, one named "tinker.ps1" that has the entire script, and one named "tinker_GUI.ps1" that has everything except the last few lines.

Now go back to the "tinker.ps1" tab and with all the text still selected, Edit / Paste.

Now you have transferred all the GUI-building pieces of the script to an external file named "tinker_GUI.ps1", and left just a little bit of code in the original file, "tinker.ps1".

But the original "tinker.ps1" no longer knows anything about the code in the external "tinker_GUI.ps1" file, so we'll have to tell it about the external file. Here now is what the "tinker.ps1" file should look like, with code in bold that you need to add:


# Offload GUI-building into a different file to decrease clutter here.
. "$PSScriptRoot\tinker_GUI.ps1"

# The computer is now looping forever waiting for user input.
# Once the input is the OK button or the Window's X (or
# keyboard equivalent), the window closes and the code below runs.
If ($Win.DialogResult -eq "OK") {
    Write-Host("The OK button was pressed. The name in the box is `"$($NameBox.Text)`".")
} elseif ($Win.DialogResult -eq "Cancel") {
    Write-Host("The X was pressed. I`'m not going to tell you the name that is in the box.")
} # end of If

Save the file, and try running it. Note that the "tinker.ps1" tab must be the active tab to run the program. If the active tab is tinker_GUI.ps1, that portion of code will run, but the tinker.ps1 portion of code won't run. In this case, it really doesn't matter all that much. That won't always be the case, though; you need to run the tab that contains the "main" piece of code.

This dot-sourcing trick essentially tells the first script to go find the referenced file and paste that file's contents in at this dot-source location. When the script runs, that second file's contents replace this line, so it's just like all that second file's contents are in this file, but without the eyeball-clutter of having those contents actually in this file.

Now we can focus just on the mechanics of working with the tree. We'll look at that in "Notes About PowerShell: Adding a TreeView to the GUI - Part 2".

Thursday, October 27, 2022

Notes About PowerShell: A More Complex GUI

Building on the simplest GUI (see here); also for more info, see The Lazy Admin), we can modify the parent window's size, title, and background color, with the following script:

# Initialize the PowerShell GUI
Add-Type -AssemblyName System.Windows.Forms
  
# Create a new window form
$ParentWindow = New-Object System.Windows.Forms.Form

# Define the size, title, and background color
$ParentWindow.ClientSize = '500,300'
$ParentWindow.text = "My Parent Window"
$ParentWindow.BackColor = "#ffffff"
  
# Display the form
$ParentWindow.ShowDialog()

Now let's add an "OK" button:

# Initialize the PowerShell GUI
Add-Type -AssemblyName System.Windows.Forms
  
# Create a new window form
$ParentWindow = New-Object System.Windows.Forms.Form

# Define the size, title, and background color
$ParentWindow.ClientSize = '500,300'
$ParentWindow.text = "My Parent Window"
$ParentWindow.BackColor = "#ffffff"
  
# The OK button.
$buttonOK = New-Object 'System.Windows.Forms.Button'
$ParentWindow.Controls.Add($buttonOK)

# Display the form
$ParentWindow.ShowDialog()

When you run this script, you should see a rectangle inside the main window. It doesn't have a label, is in the wrong spot, and doesn't do anything. Let's make some changes. Also, "$ParentWindow" is a lot of typing. We could shorten it by two letters to "$MainWindow", but we don't have any daughter- or subordinate windows, so we could just call it "$Window", but let's save even more typing:

# Initialize the PowerShell GUI.
Add-Type -AssemblyName System.Windows.Forms

# Create a new window form.
$Win = New-Object System.Windows.Forms.Form
# Define the size, title, and background color.
$Win.ClientSize = '500,300'
$Win.text = "My Parent Window"
$Win.BackColor = "#ffffff"

# The OK button.
$Okee-Dokee = New-Object 'System.Windows.Forms.Button'    # Create the button.
$Okee-Dokee.Text = "Okely-dokely, good neighbor!"         # Puts text on the button.
$Okee-Dokee.Location = '400, 240'                         # 100 pixels from the 500 edge.
$Win.Controls.Add($Okee-Dokee)                            # Add the button to the form.
# Display the form
$Win.ShowDialog() 

Better, but still not quite there. Let's add a size spec (and rename the button control back to something more meaningful):

   ...
# The OK button.
$buttonOK = New-Object 'System.Windows.Forms.Button'      # Create the button.
$buttonOK.Text = "Okely-dokely, good neighbor!"           # Puts text on the button.
$buttonOK.Location = '400, 240'                           # 100 pixels from the 500 edge.
$buttonOK.Size = '175, 23'
$Win.Controls.Add($buttonOK)                              # Add the button to the form.
   ...

And now move the button:

   ...
# The OK button.
$buttonOK = New-Object 'System.Windows.Forms.Button       # Create the button.
$buttonOK.Text = "Okely-dokely, good neighbor!"           # Puts text on the button.
$buttonOK.Location = '300, 240'                           # 200 pixels from the 500 edge.
$buttonOK.Size = '175, 23'
$Win.Controls.Add($buttonOK)                              # Add the button to the form.
   ...

Note that it doesn't matter what order these elements are defined, as long as they are defined before the "Add".

Now let's have the button do something. We'll have it close the window, and print the message "OK" to the console. (Allowable results are "None, OK, Cancel, Abort, Retry, Ignore, Yes, No".) We'll also set the button to be the "I accept the window as it is, so close it" button.

# Initialize the PowerShell GUI
Add-Type -AssemblyName System.Windows.Forms

# Create a new window form
$Win = New-Object System.Windows.Forms.Form
# Define the size, title and background color
$Win.ClientSize = '500,300'
$Win.text = "My Parent Window"
$Win.BackColor = "#ffffff"

# The OK button.
$buttonOK = New-Object 'System.Windows.Forms.Button'
$buttonOK.Anchor = 'Bottom, Right'
$buttonOK.text = "Okeley-dokely, good neighbor!"
$buttonOK.Size = '175, 23'
$buttonOK.Location = '300, 240'
$buttonOK.DialogResult = "OK"
$Win.Controls.Add($buttonOK)
$Win.AcceptButton = $buttonOK

# Display the form
$Win.ShowDialog()

Write-Host("The button was pressed, returning the response: $($Win.DialogResult)")

Now let's put a text box on the form, pre-fill it with some text, pre-select that text, and then return whatever text is in the text box when the OK button is pressed. Let's also put a label atop the text. The changes to the script are bolded below.

# Initialize the PowerShell GUI
Add-Type -AssemblyName System.Windows.Forms
  
# Create a new window form
$Win = New-Object System.Windows.Forms.Form
# Define the size, title and background color
$Win.ClientSize = '500,300'
$Win.text = "My Parent Window"
$Win.BackColor = "#ffffff"
  
# The OK button.
$buttonOK = New-Object 'System.Windows.Forms.Button'
$buttonOK.Anchor = 'Bottom, Right'
$buttonOK.text = "Okeley-dokely, good neighbor!"
$buttonOK.Size = '175, 23'
$buttonOK.Location = '300, 240'
$buttonOK.DialogResult = "OK"
$Win.Controls.Add($buttonOK)
$Win.AcceptButton = $buttonOK
  
# The Name textbox.
$NameBox = New-Object "System.Windows.Forms.Textbox"
$NameBox.Size = "175,23"
$NameBox.Location = "10,40"
$NameBox.Text = "Babushka"
$Win.Controls.Add($NameBox)
$Win.ActiveControl = $NameBox
# And its label.
$Label_NameBox = New-Object 'System.Windows.Forms.Label'
$Label_NameBox.Text = "Enter your name:"
$Label_NameBox.Size = "175,23"
$Label_NameBox.Location = '10,20'
$Win.Controls.Add($Label_NameBox)

# Display the form
$Win.ShowDialog()
  
Write-Host("The button was pressed, returning the word: $($Win.DialogResult)")

# The computer is now looping forever waiting for user input.
# Once the input is the OK button or the Window's X (or
# keyboard equivalent), the window closes and the code below runs.
If ($Win.DialogResult -eq "OK")
  {
    Write-Host("The OK button was pressed. The name in the box is `"$($NameBox.Text)`".")
  }
elseif ($Win.DialogResult -eq "Cancel")
  {
    Write-Host("The X was pressed. I`'m not going to tell you the name that is in the box.")
  } # end of If

If you wanted to pre-select just a portion of the default text of the text box:

   ...
$NameBox.Text = "Babushka"
$NameBox.SelectionStart = 4
$NameBox.SelectionLength = 3
$Win.Controls.Add($NameBox)
   ...

Notes About PowerShell: The Simplest GUI

Create the following script (in PowerShell ISE), and then run it:

# Initialize the PowerShell GUI
Add-Type -AssemblyName System.Windows.Forms
  
# Create a new window form
$MasterFrame = New-Object System.Windows.Forms.Form
  
# Display the form
$MasterFrame.ShowDialog()

That's all there is to creating a GUI window using PowerShell. Here's another rendition, just for comparison:

# Import the GUI pieces
Add-Type -AssemblyName System.Windows.Forms

# Create a new form<
$form_1 = New-Object System.Windows.Forms.Form
  
# Display the form
$form_1.ShowDialog()

For a more complex look, see here.

Tuesday, October 25, 2022

Notes About Powershell: Quickie About ADSI in Powershell on non-AD-bound Windows PC, to access AD

Two Methods for Accessing AD Info from Powershell

There are two basic methods for accessing Active Directory information from within Powershell scripts: using Active Directory Service Interfaces (ADSI), and the Powershell ActiveDirectory module.

This article is about ADSI.

ADSI is the method I'm using below. There are two basic ADSI tools used with this method: [adsi] is an accelerator (or "alias") to System.DirectoryServices.DirectoryEntry, which points to actual objects within AD, and [adsisearcher] is an accelerator to System.DirectoryServices.DirectorySearcher, which is used for searching through AD.

Using the ADSI Method for accessing information in Active Directory

Suppose you want to search, from within Powershell, for data in Active Directory on a Windows PC that is not bound to a domain, but is on the same network as a domain server.
 
You'll need to tell the Searcher what domain credentials to use.
 
To do this:
 
# Prompt user for creds; store them in $creds.UserName and $creds.Password
$creds = Get-Credential
 
# Specify the domain we want to search.
$DomainName = "LDAP://mydomain.com/DC=mydomain,DC=com" 

# Create a directory-entry object to that domain, with appropriate creds.
$DirEntry = New-Object `
     -TypeName System.DirectoryServices.DirectoryEntry `
     ArgumentList $DomainName,
     $creds.UserName,
     $($creds.GetNetworkCredential().Password)

We now have the Directory Entry object that points to the root of the Active Directory tree, along with the credentials needed for accessing that root.

Now we're ready to build our Searcher, and then to run it.

$Searcher = New-Object -type System.DirectoryServices.DirectorySearcher

Notice the similarity in types between the Searcher object and the Directory Entry object. That's the difference between [adsi] and [adsisearcher] you might see elsewhere.
 
Now we'll plug our Directory Entry object into the Searcher object:
 
$Searcher.SearchRoot = $DirEntry 

And now we'll do our search (not limiting it in any way; expect a deluge of info).
 
$Searcher.FindAll()

Splooge!

Sunday, October 23, 2022

Notes About Powershell: Getting Credentials

Sometimes when writing a Powershell script you need user credentials. Here are a few ways of doing that. We'll be using "johndoe" as the username, and "SuperSecret" as the password.

The Simplest Way - Hardcode them in your command

This is also the ugliest and least secure way. In this hypothetical case, the commands simply expect the username and password as arguments to the command:

PS > command_1 "johndoe" "SuperSecret"
PS > command_2 "johndoe" "SuperSecret"

Use Variables

Not quite as ugly, but still ugly.

PS > $UserName = "johndoe"
PS > $Password = "SuperSecret"
PS > command_1 $UserName $Password
PS > command_2 $UserName $Password

Prompt the User for a PSCredential Object

Now we're getting to a more secure option.

PS > $Creds = Get-Credential

This will pop up a window, prompting the user to enter his username and password. The script can then access the username with:

$Creds.UserName

and the password with:

$Creds.Password

Assuming our commands take credentials in the format of a PSCredential object, the commands might look like this:

PS > command_1 $Creds
PS > command_2 $Creds

But if they require an actual username/password, you might think that this will work:

PS > command_1 $Creds.UserName $Creds.Password
PS > command_2 $Creds.UserName $Creds.Password

But it won't, because the "$Creds.Password" value is in a special format itself, called a "SecureString". You can see this by simply typing the name of the variable:

PS > $Creds = Get-Credential
cmdlet Get-Credential at command pipeline position 1
Supply values for the following parameters:

PS > $Creds

UserName                     Password
--------                     --------
johndoe  System.Security.SecureString


PS > $Creds.UserName
johndoe

PS > $Creds.Password
System.Security.SecureString

But it's easy to convert that SecureString back into plain text:

PS > $Creds.GetNetworkCredential().Password
SuperSecret

So now our commands would become:

PS > command_1 $Creds.UserName $Creds.GetNetworkCredential().Password
PS > command_2 $Creds.UserName $Creds.GetNetworkCredential().Password
 
Ideally our command would be a Powershell command that natively understands the PSCredential format:

PS > PScommand_1 --Credential $Creds 

Load Up a PSCredential Object From Within the Script

This is another insecure method, but should be known about.

Since, as mentioned before, the Credential object requires a SecureString for the password, we first need to create a SecureString password:

$pwd = ConvertTo-SecureString "SuperSecret" -AsPlainText -Force

Normally the "ConvertTo-SecureString" routine expects its input to be in the format of an encrypted string of text. It's "native languages" are SecureString and Encrypted String. You can see the Encrypted String format by converting the other way:

$pwd | ConvertFrom-SecureString

which will produce something that looks like this:

01000000d08c9ddf0115d1118c7a00c04fc297eb01000000fde23916e5b4734ab18e12ca886af24900000000020000000000106600000001000020000000ff6caf244bddbe34bc6f7c8b9768317738d85d9008de0d3acaac4ee9133bc4870
00000000e80000000020000200000008ce0cd22aa3fcbb938f08f640201e1bb1c91500b679346a02e48815a385101a2200000006e2055f816b37e010e1938e59938d244c6170307ef651db8e95920f0c06b16d540000000f6b72c9148c628
c579209cd761fd0a2d6ebbdfbae888462733b06f08aec293e02f3a62b83ded1a10d24e370ab5b584bc31bf1816273e8761577b0a7455545881

So to be clear, the "ConvertTo.../...From..." routines convert (natively) between SecureString and Encrypted Strings, not to/from plain text. To include the plain text in the process, we have to tell the "ConvertTo..." routine that its input is "AsPlainText", and to get the plain text back out, we have to use a completely different routine, as we did above, the "GetNetworkCredential()" routine.

Now that we have the password as a SecureString, we can create the PSCredential object:

$Creds = New-Object System.Management.Automation.PSCredential ("johndoe", $pwd)

And now you can use the PSCredential object just as we did above:

PS > command_1 $Creds.UserName $Creds.GetNetworkCredential().Password
PC > PScommand_1 --Credential $Creds

Load Up a PSCredential Object From an External File

This is kind of a compromise between the security of prompting the user for the credentials and having the credentials associated with the script itself. The password will be in an Encrypted format, and in a separate file, but it would still be pretty easy for a "hacker" to grab the file and read the password. So it's certainly not secure. You could put the external file on a secure server, but then you'd have to deal with the credentials for logging into the secure server, so all that does is kick the problem down the road a ways. Still, it might be the best we can do without going to extremes.

But the most onerous caveat about this method, at least for general purpose scripts, is that the password encryption can only be decrypted by the same user account on the same computer as was used to do the encryption. You can't write/develop the script that uses this method on one computer, and then run it on another computer, or as a different user.

Some of the following is likely a repeat of what was said above; it's a copy-and-paste from another article I had started/

Reading the Creds From a File

If you don't have a user sitting in front of the computer when the script runs, you can store the username and an encrypted form of the password in a file, beforehand, and then read it from that file when it's needed. Be aware that this is still not secure, but it's more so.

Saving the Credentials to an External File, Beforehand

Not in your script, but just at a Powershell prompt, enter the following:

PS > Set-Content -Path ".\extras.zip" -Value "johndoe"

This step creates a file named "extras.zip", over-writing any existing files of that name, in the current directory. The file contains the username. You can verify the contents of this newly-created file with:

PS > type ".\extras.zip"

As you can see, it's not really a .zip file; it's just a plain text file. But naming it as a .zip is a minor mindgame, hoping to discourage the casual "hackers" from bothering to try and open / look into the file. It's a weak form of "security via obscurity"; probably completely useless, but I know when I'm casually looking at files with which I'm unfamiliar, I tend to look at .txt files and to leave .zip files along. But you can name your file any way you want.

PS > Add-Content -Path ".\extras.zip" -Value $("SuperSecret") | ConvertFrom-SecureString -AsPlainText -Force)

This step adds the username to the now-already-existing "extras.zip" file. The value that gets added to the file is the password, which before getting added to the file, is piped to a converter that takes the plain text of "SuperSecret" and converts it to a SecureString.

You should now have a file named "extras.zip" with something like the following (which you can see with "type .\extras.zip":

johndoe 01000000d08c9ddf0115d1118c7a00c04fc297eb010000001150a84662a6cd45af512286977fabcc00000000020000000000106600000001000020000000943962d675717d4a12cfe44a8a22b6a5f0ca77762c7bb61e6929515af684db5d000000000e80000000020000200000006370779b6f82983d4f417eccad5be5a80b0ae8a71d2820e736862d2b25453ce220000000c96767ea12fd28ab840fcefb49b4f86d84e97f4676e806a8d7511a86532d3c3f400000008a26d44b1d1df760436be9be186cb87a6cca6220364752e435af61188330043ac201f634ff88fd751b17aff9394c00e6ada09a2f1dd93c27702f9802e813f90d

Normally the ConvertTo-SecureString expects its input to be a standard encrypted string of plain text, like the line with numbers above. But we're starting with the plain text of "SuperSecret". If you feed it this plain unencrypted text, it complains. You can see this for yourself with this command:

PS > "SuperSecret" | ConvertTo-SecureString

That's why we use the "-AsPlainText" argument, to override that behavior. You can see this for yourself like so:

PS > 'SuperSecret" | ConvertTo-SecureString -AsPlainText

But then you get another complaint. For security reasons, the command doesn't like to be given secret info out in the open, but you can force it to accept the input anyway, with:

PS > 'SuperSecret" | ConvertTo-SecureString -AsPlainText -Force

and now you see that the result is a SecureString.

However, we can not write this SecureString out to a file, and then recover it later. But what we can do is reverse the process, part-way. We won't reverse it back to a plain-text "SuperSecret", but rather to an encrypted-text form of "SuperSecret", by piping the above command to the counterpart:

PS > 'SuperSecret" | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString

The "native language" of these two tools is SecureStrings on one side and encrypted text on the other, so we don't have to specify anything special; it just takes the SecureString in the previous command and converts from that into encrypted text.

Now that we have the credentials stored in an external file, we have to tell our script to read them.

Reading the Credentials from an External File
PS > $contents = Get-Content -Path ".\extras.zip"
PS > $Creds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $contents[0], $($contents[1] | ConvertTo-SecureString)

Use the PSCredential Object

And now you can use the PSCredential object just as we did above:

PS > command_1 $Creds.UserName $Creds.GetNetworkCredential().Password
PC > PScommand_1 --Credential $Creds

Tuesday, October 18, 2022

Set Up Bridged Networking on Debian for QEMU/KVM (Virtual Machine Manager)

Step One: Create a New Bridged Network Interface

Without the VM running, create a virtual bridged interface. Create a file named "br0" in /etc/networking/interfaces.d, with the following contents:

auto br0
iface br0 inet dhcp
   pre-up ip tuntap add dev tap0 mode tap user <username who will run virtual machine>
   pre-up ip link set tap0 up
   bridge_ports all tap0
   bridge_stp off
   bridge_maxwait 0
   bridge_fd      0
   post-down ip link set tap0 down
   post-down ip tuntap del dev tap0 mode tap

Step Two: Restart networking.

Before you restart networking, and then again after you restart networking, you can run:

ip link show type bridge

and

ip link show master br0

to see some before-and-after stats on what you're accomplishing.

sudo systemctrl restart networking

or

sudo /etc/init.d/networking restart

Start Virtual Machine Manager and configure the VM to use the new bridged interface.

Once the Virtual Machine Manager is running, Open the desired VM, and then in the View menu, select Details. Select the NIC... item in the left-hand pane, and then in the right-hand pane, change the Network Source from "Virtual network 'default' : NAT" to "Bridged device...", and then in the Device name: field, enter "br0" to match the name of the interface defined in Step One above.

Now power-on your VM, and it should connect via bridged networking instead of being NAT-ted.

CAVEAT:  As I discovered later, this does not work with a wi-fi host; the host needs to be wired to Ethernet. I think it can be made to work (at least in some cases), but it looks to be complicated.

Monday, October 17, 2022

Notes About Powershell: Very Basic Powershell ISE Usage

Start by opening the Powershell ISE. It should be available in the "Type here to search" window of Windows 10, or similar. Note that the ISE (Integrated Scripting Environment) is essentially a simple Integrated Development Environment (IDE). Initially, it'll likely open only to a blue screen where you are in the Powershell "shell", where you can type commands, etc. For example, type:

dir

to get a listing of the current directory. This is a backwards-compatible (to the days of MS-DOS, even) command. The Powershell equivalent of doing the same thing is:

Get-Childitem

Yes, that does seem like over-kill, but Microsoft is going for consistency within the Powershell environment, with every command having an approved verb, Get in this case, followed by the "gist" of what the command is about, with the Childitem referring to the directory items.

You can put these Powershell-type commands in a script, to run like a program. To do this, go to File/New to open a new script window.

In here, you can put your dir command. Or better, use the Powershell equivalent: Get-Childitem. You might see the ISE trying to help you find the correct command/item with it's type-ahead prompts. (And you might not; it seems to be pretty hit-or-miss.)

Now, run your script by clicking on the green arrow in the menubar area. The results should show in the blue shell area below the text editing area.

Now if you save the script to a file, it'll automatically save with a ".ps1" extension, and you can open it later for editing. But once you save it, Powershell considers it a "real" script, and will refuse to run it because running scripts is against the default Powershell policy. Give it a try to see what I mean (assuming it hasn't already been fixed on your machine).

To fix that, fire up a Powershell session as Administrator, and run:

PS > Set-ExecutionPolicy

and select the "All" option. You can then exit out of the Admin session of Powershell, and restart your normal-user session of the Powershell ISE, and you should now be able to run your script.

Notes About Powershell: Accelerators

Accelerators

It is my understanding that many items in square brackets are "accelerators", which are what I would know as "aliases". You can get a list of Powershell objects that are accelerators like this:

PS >  [System.Management.Automation.PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::get

Key                          Value                                                              
---                          -----                                                              
Alias                        System.Management.Automation.AliasAttribute                        
AllowEmptyCollection         System.Management.Automation.AllowEmptyCollectionAttribute         
AllowEmptyString             System.Management.Automation.AllowEmptyStringAttribute             
AllowNull                    System.Management.Automation.AllowNullAttribute                    
ArgumentCompleter            System.Management.Automation.ArgumentCompleterAttribute            
array                        System.Array                                                       
bool                         System.Boolean
  ...
pslistmodifier               System.Management.Automation.PSListModifier                        
psobject                     System.Management.Automation.PSObject                              
pscustomobject               System.Management.Automation.PSObject                              
psprimitivedictionary        System.Management.Automation.PSPrimitiveDictionary                 
  ...
CimSession                   Microsoft.Management.Infrastructure.CimSession                     
adsi                         System.DirectoryServices.DirectoryEntry                            
adsisearcher                 System.DirectoryServices.DirectorySearcher                         
wmiclass                     System.Management.ManagementClass                                  
wmi                          System.Management.ManagementObject                                 
wmisearcher                  System.Management.ManagementObjectSearcher                         
mailaddress                  System.Net.Mail.MailAddress                                        
scriptblock                  System.Management.Automation.ScriptBlock                           
psvariable                   System.Management.Automation.PSVariable                            
  ...

Notice the psobject accelerator. It's an accelerator ("alias") to System.Management.Automation.PSObject. This means that the above command can be shortened to:

PS >   [psobject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::get

Read more about accelerators here.

Saturday, October 15, 2022

Notes About Powershell, adsi, Active Directory Module, alternative authentication, Basics of Accessing Active Directory

Two Methods for Accessing AD Info from Powershell

There are two basic methods for accessing Active Directory information from within Powershell scripts: using Active Directory Service Interfaces (ADSI), and the Powershell ActiveDirectory module.

The second method, using the ActiveDirectory module, is native to Powershell, but is not "built-in" to most Powershell installations. Therefore, if you're planning for your Powershell to run on multiple computers, you have to take actions to make sure tht module is installed. This adds complexity to your script, and possible time to the run-time of the script.

The first method is "built in" to Powershell (sort of), but not native to Powershell. It is basically an import from the .NET system that is already installed on most Windows systems. It is this immediate and reliable availability that makes me prefer the ADSI method over the ActiveDirectory module method. Otherwise I'd stick with the pure Powershell method.

ADSI is the method I'm using below. There are two basic ADSI tools used with this method: [adsi] is an accelerator (or "alias") to System.DirectoryServices.DirectoryEntry, which points to actual objects within AD, and [adsisearcher] is an accelerator to System.DirectoryServices.DirectorySearcher, which is used for searching through AD.

Using the ADSI Method for accessing information in Active Directory

On a Machine Already Bound to an AD Domain

To access Active Directory (AD) data, we must bind to the directory. If your computer is attached to a network on which resides an AD controller, and your computer is already bound to that controller's domain, and you're logged into that domain, you can simply run:

[adsi]''

at a Powershell prompt, or from a Powershell script. This command will return the distinguishedName of the domain to which the computer is bound. Your results should be something like this:

distinguishedName : {DC=acu,DC=local}
  Path              : 

adsi is an accelerator ("alias" - see here for more info) for System.DirectoryServices.DirectoryEntry. The equivalent command is:

[System.DirectoryServices.DirectoryEntry]''

I mention using adsi because you might see it elsewhere. But for clarity, I'll use the more verbose verbiage, System.DirectoryServices.DirectoryEntry.

On a Machine Not Bound to an AD Domain

If your computer is not bound to an AD controller, you'll get results like this:

PS C:\Users\westk> [adsi]""
format-default : The following exception occurred while retrieving member "distinguishedName": "The specified domain either does not exist or could not be 
contacted.
"
    + CategoryInfo          : NotSpecified: (:) [format-default], ExtendedTypeSystemException
    + FullyQualifiedErrorId : CatchFromBaseGetMember,Microsoft.PowerShell.Commands.FormatDefaultCommand

(Notice the quotes can be single or double; sometimes one will work better than the other depending on the situation.)

The computer I'm working with is not currently bound to the domain, but it is on the same network as the domain. In order to get AD info, I have to bind the computer in some way to the domain. This will require credentials for logging into the domain. If the computer were already on the domain, and I logged in as a domain user, this method would use my Windows login credentials by default. But we can specify different credentials, or just provide credentials if the computer is not already on the domain.

The System.DirectoryServices.DirectoryEntry method is picky about how credentials are presented. You have to give it the type of connection being made ("LDAP", as opposed to "WinNT", or one other method which I can't recall at the moment), and a username and a password that has domain permissions to read from the domain. For reading from this part of the domain, almost any domain user will suffice.

When giving this data to System.DirectoryServices.DirectoryEntry, the object it returns is "not compatible" with just spitting out the results to the console like it is when not giving this information; you'll need to declare the results as a new object. We'll call the new object "$root", since we're starting at the root of the Active Directory domain tree. If we wanted, we could call it "domain", or "DomainDN", or "bub". Suppose the username you're using to bind with is "johndoe", and his password is "SuperSecret". The command could thus look like this:

$root = New-Object System.DirectoryServices.DirectoryEntry("LDAP://acu.local/DC=acu,DC=local","acu.local\johndoe","SuperSecret")

Notice that the username field includes the domain name component.

When we look at the results:

PS > $root

this should produce output like this:

distinguishedName : {DC=acu,DC=local}
Path              : LDAP://acu.local/DC=acu,DC=local

We could also write the one-liner variable-assignment as several lines (ending some lines with a back-tic to signify that the line is continued), perhaps making the declaration more readable:

$root = New-Object `
	-TypeName System.DirectoryServices.DirectoryEntry `
	-ArgumentList "LDAP://acu.local/DC=acu,DC=local",
	"acu.local\johndoe",
	"SuperSecret"

Being a Little More Secure With the Password

Although you can feed the credentials directly to this command as strings, that's not a very secure way to do it in a script. So let's use variables instead, including a variable for the path within the AD tree:

$UserName = "johndoe"
$Password = "SuperSecret"
$AD_Path = "LDAP://acu.local/DC=acu,DC=local"
$root = New-Object `
	-TypeName System.DirectoryServices.DirectoryEntry `
	-ArgumentList $AD_Path,
	$UserName,
	$Password

Now we'd need to do something about those credential assignments being out in the open like that.

Secure-Prompting the User

If your script is going to be interactively run by a user sitting in front of the computer, you can (securely) prompt the user for the creds, like so:

$creds = Get-Credential
$root = New-Object `
	-TypeName System.DirectoryServices.DirectoryEntry `
	-ArgumentList $AD_Path,
	$($creds.UserName),
	$($creds.GetNetworkCredential().Password)

(You can also prompt with a pre-loaded username field with $creds = Get-Credential -Credential "acu.local\johndoe" . Note also that this process doesn't do anything; it just puts these two values in the $creds variable, making the previous variables, $UserName and $Passord, superflous; get rid of them from your script.)

Note that the $creds.Password must be manipulated a bit to get it in an acceptable form to be used by this command. Looking at the values of the various objects...

PS C:\Users\westk> $creds

UserName                            Password
--------                            --------
acu.local\johndoe System.Security.SecureString


PS C:\Users\westk> $creds.UserName
acu.local\johndoe

PS C:\Users\westk> $creds.Password
System.Security.SecureString

PS C:\Users\westk> $creds.GetNetworkCredential().Password
SuperSecret

you can start to understand the manipulations involved.

Reading the Creds From a File

If you don't have a user sitting in front of the computer when the script runs, you can store the username and an encrypted form of the password in a file, beforehand, and then read it from that file when it's needed. Be aware that this is still not secure, but it's more so.

Also, the most onerous caveat about this method, at least for general purpose scripts, is that the password encryption can only be decrypted by the same user account on the same computer as was used to do the encryption. You can't write/develop the script that uses this method on one computer, and then run it on another computer, or as a different user.

Saving the Credentials to an External File, Beforehand

Not in your script, but just at a Powershell prompt, enter the following:

Set-Content -Path ".\extras.zip" -Value "acu.local\johndoe"

This step creates a file named "extras.zip", over-writing any existing files of that name, in the current directory. The file contains the username. You can verify the contents of this newly-created file with:

type ".\extras.zip"

As you can see, it's not really a .zip file; it's just a plain text file. But naming it as a .zip is a minor mindgame, hoping to discourage the casual "hackers" from bothering to try and open / look into the file. It's a weak form of "security via obscurity"; probably completely useless, but I know when I'm casually looking at files with which I'm unfamiliar, I tend to look at .txt files and to leave .zip files along. But you can name your file any way you want.

Add-Content -Path ".\extras.zip" -Value $("SuperSecret") | ConvertFrom-SecureString -AsPlainText -Force)

This step adds the username to the now-already-existing "extras.zip" file. The value that gets added to the file is the password, which before getting added to the file, is piped to a converter that takes the plain text of "SuperSecret" and converts it to a SecureString.

You should now have a file named "extras.zip" with something like the following (which you can see with "type .\extras.zip":

acu.local\johndoe
01000000d08c9ddf0115d1118c7a00c04fc297eb010000001150a84662a6cd45af512286977fabcc00000000020000000000106600000001000020000000943962d675717d4a12cfe44a8a22b6a5f0ca77762c7bb61e6929515af684db5d000000000e80000000020000200000006370779b6f82983d4f417eccad5be5a80b0ae8a71d2820e736862d2b25453ce220000000c96767ea12fd28ab840fcefb49b4f86d84e97f4676e806a8d7511a86532d3c3f400000008a26d44b1d1df760436be9be186cb87a6cca6220364752e435af61188330043ac201f634ff88fd751b17aff9394c00e6ada09a2f1dd93c27702f9802e813f90d

Normally the ConvertTo-SecureString expects its input to be a standard encrypted string of plain text, like the line with numbers above. But we're starting with the plain text of "SuperSecret". If you feed it this plain unencrypted text, it complains. You can see this for yourself with this command:

"SuperSecret" | ConvertTo-SecureString

That's why we use the "-AsPlainText" argument, to override that behavior. You can see this for yourself like so:

'SuperSecret" | ConvertTo-SecureString -AsPlainText

But then you get another complaint. For security reasons, the command doesn't like to be given secret info out in the open, but you can force it to accept the input anyway, with:

'SuperSecret" | ConvertTo-SecureString -AsPlainText -Force

and now you see that the result is a SecureString.

However, we can not write this SecureString out to a file, and then recover it later. But what we can do is reverse the process, part-way. We won't reverse it back to a plain-text "SuperSecret", but rather to an encrypted-text form of "SuperSecret", by piping the above command to the counterpart:

'SuperSecret" | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString

The "native language" of these two tools is SecureStrings on one side and encrypted text on the other, so we don't have to specify anything special; it just takes the SecureString in the previous command and converts from that into encrypted text.

Now that we have the credentials stored in an external file, we have to tell our script to read them.

Reading the Credentials from an External File

In your script, replace the "$creds = Get-Credential" line like so:

# $creds = Get-Credential
$contents = Get-Content -Path ".\extras.zip"       # Read the file's contents.
# Reconvert the encrypted password back to a SecureString, and put both username and password into a new PSCredential object named $creds.
$creds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $contents[0], $($contents[1] | ConvertTo-SecureString)

A Whole Script

Here's a whole script, so you can see the big picture.

<#  Powershell Script To Access An Active Directory Tree
  Written By:  [your name]
  Date: [today's date]
  [any ther comments you want to add]
#>
  
$creds = Get-Credential
$AD_Path = "LDAP://acu.local/DC=acu,DC=local"
$root = New-Object `
	-TypeName System.DirectoryServices.DirectoryEntry `
	-ArgumentList $AD_Path,
	$($creds.UserName),
	$($creds.GetNetworkCredential().Password)
Write-Output("The Distinguished (`"unique`") Name for the AD domain is: $($root.distinguishedName)")

Save that as a Powershell script file, and run it, and you should see output similar to:

The Distinguished ("unique") Name for the AD domain is: DC=acu,DC=local

A Second Whole Script, Slightly Different, for Perspective

#  Powershell Script To Access An Active Directory Tree
$Username_And_Password = [PSCustomObject]@{
	UserName = "maryjane"
	Password = "SuperTramp"
}
$TreeRoot = "LDAP://acu.local/DC=acu,DC=local"
$DirEntry = New-Object `
	-TypeName System.DirectoryServices.DirectoryEntry `
	-ArgumentList $($TreeRoot),
	$($Username_And_Password.UserName),
	$($Username_And_Password.Password)
Write-Output("The root is: $($TreeRoot).")
Write-Output("The password for $($Username_And_Password.UserName) is $($Username_And_Password.Password).")

One More Example

$AD_Node = adsi New-Object ("LDAP://DC=my_company,DC=com","my_company\accountant","time=$")
Write-Output("The domain info = $($AD_Node | Get-Member).")

which will produce output like this:

The domain info = static string ConvertDNWithBinaryToString(psobject deInstance, psobject dnWithBinaryInstance) static
 long ConvertLargeIntegerToInt64(psobject deInstance, psobject largeIntegerInstance) System.DirectoryServices.Property
ValueCollection auditingPolicy {get;set;} System.DirectoryServices.PropertyValueCollection creationTime {get;set;} Sys
tem.DirectoryServices.PropertyValueCollection dc {get;set;} System.DirectoryServices.PropertyValueCollection distingui
shedName {get;set;} System.DirectoryServices.PropertyValueCollection dSASignature {get;set;} System.DirectoryServices.
PropertyValueCollection dSCorePropagationData {get;set;} System.DirectoryServices.PropertyValueCollection forceLogoff 
{get;set;} System.DirectoryServices.PropertyValueCollection fSMORoleOwner {get;set;} System.DirectoryServices.Property
ValueCollection gPLink {get;set;} System.DirectoryServices.PropertyValueCollection instanceType {get;set;} System.Dire
ctoryServices.PropertyValueCollection isCriticalSystemObject {get;set;} System.DirectoryServices.PropertyValueCollecti
on lockoutDuration {get;set;} System.DirectoryServices.PropertyValueCollection lockOutObservationWindow {get;set;} Sys
tem.DirectoryServices.PropertyValueCollection lockoutThreshold {get;set;} System.DirectoryServices.PropertyValueCollec
tion masteredBy {get;set;} System.DirectoryServices.PropertyValueCollection maxPwdAge {get;set;} System.DirectoryServi
ces.PropertyValueCollection minPwdAge {get;set;} System.DirectoryServices.PropertyValueCollection minPwdLength {get;se
t;} System.DirectoryServices.PropertyValueCollection modifiedCount {get;set;} System.DirectoryServices.PropertyValueCo
llection modifiedCountAtLastProm {get;set;} System.DirectoryServices.PropertyValueCollection ms-DS-MachineAccountQuota
 {get;set;} System.DirectoryServices.PropertyValueCollection msDS-AllUsersTrustQuota {get;set;} System.DirectoryServic
es.PropertyValueCollection msDS-Behavior-Version {get;set;} System.DirectoryServices.PropertyValueCollection msDS-Expi
rePasswordsOnSmartCardOnlyAccounts {get;set;} System.DirectoryServices.PropertyValueCollection msDS-IsDomainFor {get;s
et;} System.DirectoryServices.PropertyValueCollection msDs-masteredBy {get;set;} System.DirectoryServices.PropertyValu
eCollection msDS-NcType {get;set;} System.DirectoryServices.PropertyValueCollection msDS-PerUserTrustQuota {get;set;} 
System.DirectoryServices.PropertyValueCollection msDS-PerUserTrustTombstonesQuota {get;set;} System.DirectoryServices.
PropertyValueCollection name {get;set;} System.DirectoryServices.PropertyValueCollection nextRid {get;set;} System.Dir
ectoryServices.PropertyValueCollection nTMixedDomain {get;set;} System.DirectoryServices.PropertyValueCollection nTSec
urityDescriptor {get;set;} System.DirectoryServices.PropertyValueCollection objectCategory {get;set;} System.Directory
Services.PropertyValueCollection objectClass {get;set;} System.DirectoryServices.PropertyValueCollection objectGUID {g
et;set;} System.DirectoryServices.PropertyValueCollection objectSid {get;set;} System.DirectoryServices.PropertyValueC
ollection otherWellKnownObjects {get;set;} System.DirectoryServices.PropertyValueCollection pwdHistoryLength {get;set;
} System.DirectoryServices.PropertyValueCollection pwdProperties {get;set;} System.DirectoryServices.PropertyValueColl
ection replUpToDateVector {get;set;} System.DirectoryServices.PropertyValueCollection repsFrom {get;set;} System.Direc
toryServices.PropertyValueCollection repsTo {get;set;} System.DirectoryServices.PropertyValueCollection rIDManagerRefe
rence {get;set;} System.DirectoryServices.PropertyValueCollection serverState {get;set;} System.DirectoryServices.Prop
ertyValueCollection subRefs {get;set;} System.DirectoryServices.PropertyValueCollection systemFlags {get;set;} System.
DirectoryServices.PropertyValueCollection uASCompat {get;set;} System.DirectoryServices.PropertyValueCollection uSNCha
nged {get;set;} System.DirectoryServices.PropertyValueCollection uSNCreated {get;set;} System.DirectoryServices.Proper
tyValueCollection wellKnownObjects {get;set;} System.DirectoryServices.PropertyValueCollection whenChanged {get;set;} 
System.DirectoryServices.PropertyValueCollection whenCreated {get;set;}.

Get The Next Level of Objects in the Domain Tree

After the last example, we now have the top lovel of our domain ("acu" in my case) in the variable $TreeRoot (or "$AD_Path", or "$root", or "AD_Node", or whatever variable name you chose to use; I'll use $Monkey in the following text, because we'll be watching where the monkey has climbed to in the tree).

Also, I have replaced the multi-line definition of the $Monkey object with the one-liner version.

Here's the script I currently have:

<# Scan_AD_Tree

    .DESCRIPTION
    This script accesses an Active Directory Tree

    .AUTHOR
    Kent West
    October 2022
#>

# These creds of a domain user will be used to bind to the domain.
# We can either prompt the user for those credentials...
# $creds = Get-Credential

# ... or we can get the creds from an external text file, assuming the file has been previously created, with:
# Set-Content -Path ".\extras.zip" -Value "acu.local\johndoe"
# Add-Content -Path ".\extras.zip" -Value $("SuperSecret") | ConvertFrom-SecureString -AsPlainText -Force)
$contents = Get-Content -Path ".\extras.zip"   # This reads the file, auto-making the "$contents" an array variable.
# Reconvert the encrypted-text password back to a SecureString, and put both username and password into a new PSCredential object named $creds.
$creds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $contents[0], $($contents[1] | ConvertTo-SecureString)

# This is the LDAP "path" to the root of our AD domain "tree" named "acu.local".
$Tree_Path = "LDAP://acu.local/DC=acu,DC=local"

# We'll bind our tree-climber ("$Monkey") to the tree's path, currently at the root, using the domain user's creds.
$Monkey = New-Object System.DirectoryServices.DirectoryEntry($Tree_Path,$creds.UserName,$creds.GetNetworkCredential().Password)

Write-Output("The Common Name (CN) of the domain is $($Monkey.Name)."

Explanations/Comments in the Above May Not Be Entirely Accurate

This has been a learning process for me; I'm not confident I've explained things above entirely correctly, and I'm tired of chasing this rabbit, so I'm going to stop here, with a link that I started to include, along with a piece of code that references itself for no reason I can think of. In the hidden comments of the source to this page is a lot more information that doesn't really belong here, but is related.

Source Info
$DomainDN = New-Object -Type System.DirectoryServices.DirectorySearcher
$DomainDN.SearchRoot.Path

Sunday, October 02, 2022

Acceptable Worship

Acceptable worship used to be a matter of bowing down before the correct altar in the correct temple in the correct mountain.

Now, it's no longer a matter of bowing in this church building on this hill or that church building on that hill, but of truthful bowing in your spirit.

--Jesus, paraphrased

The Right Tool for the Job

I prefer acappella in our assemblies, for practical reasons.

Instruments (and "praise teams") tend to kill congregational singing. It tends to move congregants from the role of participant to the role of spectator.

But I'm very much against abusing scripture, and against making laws which God has not made.

Since the primary goal of assembling is to provoke one another to love and good works, and to encourage one another, and to build up one another, whatever accomplishes that is fair game within the assembly (1 Cor 14:26; Heb 10:24-25). God has made us all different, with different giftings. Some of us are brains, functioning on logic and reason; some of us are ears, functioning on music; some of us are eyes, functioning on art. Different members, and different "moods" within a member, will be best nourished by different foods than will be another member, or "mood". Like in the very earliest church, when sometimes the Jewish believers focused on study in the synagogue, and sometimes they praised God in the "raucous" environment of the Temple, modern Christians need the "everyone participates" congregational singing, and sometimes they need the instrumental-backed singing in the car that drive God-concepts deep into the brain so that those concepts bubble up in your background thinking. Stephen Spielberg does not make movies without instrumental music, because he knows that music has power to communicate very strongly, even without words. Believers should not throw away such a powerful tool that can be very valuable in the task of provoking, encouraging, and edifying, simply because we insist on teaching as doctrine a commandment we've invented which God has not spoken and which violates the principles he has spoken.

But it takes skill to wield instrumental music as that sort of powerful tool, and "praise bands" in our assemblies *generally* do not foster what is supposed to be fostered in our assemblies, at least for me, and at least in my experience. For me, in my experience, acappella, congregational, four-part harmony singing serves that task better.

But I'm not an ear.

Unless a tool has been forbidden (and instrumental music has not been forbidden), use whatever tool best does the job we've been assigned to do. If it provokes, encourages, and/or builds up, use the tool, whether that is a guitar, or a PowerPoint presentation, or a live art illustration, or a skit where one ties a belt around his hands to mime being arrested.

Just Christians

It has been claimed (I've not done the research myself) that US President Teddy Roosevelt was in favor of legal immigration, with the stipulation that the immigrant endeavor to become fully integrated into America, learning the language and the history of America, and becoming an American, not a This-American or a That-American, which spurs division rather than unity.

As I have understood it, this is the essential thrust of the Restoration Movement, to become a Christian, not a This-Christian or a That-Christian, not a Baptist Christian or a Paulite Christian or even a Church of Christ-er Christian.

I believe this is a good goal, and even though the implementation of this goal has been marred by our imperfections, the "Church of Christ" "denomination" at least pays lip-service to this goal.

Continue to reject the failures in the attempts by the Church of Christ-er Christians towards this goal, but I encourage you to join with them in the goal itself - just be a Christian, not a Flavored-Christian, which spurs division rather than unity.

Thursday, August 11, 2022

The Meaning of 'ekklesia'

ekklesia, usually translated as "church" in most English versions of the Bible, is a compound word meaning "out of" (ek) and "called" (kaleo). Etymologically it means "the called out".


Whereas it's unwise to accept the etymological meaning of a word as its actual meaning ("pineapple" has nothing to do with either pines or apples [except somewhat for it's appearance in the eyes of medieval botanists]), in this case it's pretty safe.


The earliest usage of the word appears to date back to early Greek City-States, and refers to political restructuring, when citizens might be "called out" of a city to form, as a group, a new government structure. A political coup, so to speak.


By New Testament times it was a little less political, a little less revolutionary, sometimes referring to simply a crowd, even a rowdy one, like when the town clerk dismisses the rioting mob in Acts 19:41, using the word ekklesia.


(The "churches of Christ" in Rom 16:16 could be rendered just as accurately as the "mobs of the Messiah".)


When Jesus announced that he would build his church, he was essentially saying that he was going to call out from the world a people of his own, to form a new government, and we see this sort of description both in earlier scripture:

WEB Isa 9:7 Of the increase of his government and of peace there shall be no end, on David’s throne, and on his kingdom, to establish it, and to uphold it with justice and with righteousness from that time on, even forever.
and later scripture:
WEB 1 Pet 2:9 But you are a chosen race, a royal priesthood, a holy nation, a people for God’s own possession, that you may proclaim the excellence of him who called you out of darkness into his marvelous light.
Originally published at:
http://kentwest.blogspot.com/2022/08/the-meaning-of-ekklesia.html

Christianity Was Exclusively Jewish in the Beginning

Some believe that when the earliest Christians were in the temple of the Jews, they were there only to preach, and no longer behaved as Jews did within the temple.
 
How did Theophilus understand the words of Luke in Acts 2:46-47?
 
You will recall that the book of Acts is essentially The Book of Luke to Theophilus, Vol 2. What did Theophilus learn from Vol 1?
Luke 24:52-53 (WEB) They worshiped him, and returned to Jerusalem with great joy, and were continually in the temple, praising and blessing God. Amen.
When Theophilus later read:
Acts 2:46-47 (WEB) Day by day, continuing steadfastly with one accord in the temple, and breaking bread at home, they took their food with gladness and singleness of heart, praising God, and having favor with all the people. The Lord added to the assembly day by day those who were being saved.
it is unlikely that he understood the disciples to not be praising God in the temple, but rather to be praising him elsewhere.
 
Rather, he probably understood them to be praising God in the temple and in their homes, both. There is no reason to understand Luke as saying, "They praised God in the temple, until Pentecost, and then they stopped praising God in the temple and praised him only in their homes, but I'm not going to clearly indicate that."
 
We know that after questioning by the temple authorities, Peter and John "returned to their company", and that a congregational prayer was then offered up to God (Acts 4:23ff), and that their company congregated in the Porch of Solomon (Acts 3:11; 5:11-12).
 
Paul was in the temple praying (Acts 22:17), not "disputing with anyone or stirring up a crowd" (24:12). The temple was a "house of prayer" (Matt 21:13), and that's exactly the purpose for which Paul used it; he did not use it at all as a preaching location, which tells us that the Apostles were not in the temple only to preach.
 
The text indicates that the Jewish believers continued being Jewish - they kept circumcising their children, and they kept their Jewish customs (Acts 21:17ff). Even Peter at first resisted the call to go to the "unclean" Gentiles, because that was "against the law" (Acts 10, esp v. 28), and it required a heavenly vision three times to overcome his resistance. The Jewish brothers with Peter were astonished that God's favor was no longer exclusively for the Jewish, but "also" for the Gentiles (Acts 10:45). The other church leaders in Jerusalem also initially condemned Peter for his visit to an unclean Gentile (Acts 11:3), but finally admitted that Gentiles were now welcome into the church:
Acts 11:18 (WEB) When they heard these things, they held their peace, and glorified God, saying, “Then God has also granted to the Gentiles repentance to life!”
It seems clear that in the beginning, the earliest Christians believed that Christianity was exclusively Jewish.
 
Fourteen years later, there was still a question of what was required of Gentiles to become Christians, and a significant portion of brethren believed that such required the Gentiles to become Jews, via circumcision and the keeping of the law of Moses:
Acts 15:1 (WEB) ​ Some men came down from Judea and taught the [Gentile] brothers, “Unless you are circumcised after the custom of Moses, you can’t be saved.” ... Acts 15:5 But some of the sect of the Pharisees who believed rose up, saying, “It is necessary to circumcise [the Gentile believers], and to command them to keep the law of Moses.”
And here's the kicker: the Apostles did not have a definitive, authoritative answer for the question about Gentile believers. It took "much discussion" (Acts 15:7) to come to a decision; not to a "thus saith the Lord", but rather to a "it seemed good to the holy spirit and to us" (Acts 24:28) "decision" (Acts 21:25). Here we see put into action the promise to Peter that whatever is bound on earth will be bound in heaven.
 
So what message does the Bible present? That in the very beginning, the church was exclusively Jewish, continuing to do things in the Jewish ways, abiding by the law of Moses, believing that circumcision and the keeping of the law of Moses was required to be a Christian. It took visions and miracles and much discussion and years to realize that Gentiles don't have to become Jewish to be saved.
 
When we get to Paul in Acts 21, we find this exact same situation: the Jewish believers were zealous for the law of Moses and their customs, but the Gentile believers were not bound by these things:
Acts 21:20 (WEB) They, when they heard it, glorified God. They said to him, “You see, brother, how many thousands there are among the Jews of those who have believed, and they are all zealous for the law. ... Acts 21:25 But concerning the Gentiles who believe, we have written our decision that they should observe no such thing, except that they should keep themselves from food offered to idols, from blood, from strangled things, and from sexual immorality.”
I believe it is incorrect to believe that the earliest Christians stopped behaving in the temple as Jews behaved within the temple. They were Jews within the temple; they were not something else. What was new about these Jews was not that they behaved differently than the other Jews in the temple, but that they had found their long-awaited Chosen One, their Messiah.
 
Originally published at:
http://kentwest.blogspot.com/2022/08/christianity-was-exclusively-jewish-in.html

Thursday, July 28, 2022

The Letter of the Law vs The Spirit of the Law

Did you know ...

In at least one case, God cared more about the person's heart than he did about the person "obeying the command to the letter":
WEB 2 Chron 30:18 For a multitude of the people ... had not cleansed themselves, yet they ate the Passover other than the way it is written. For Hezekiah had prayed for them, saying, “May the good Yahweh pardon everyone who sets his heart to seek God, Yahweh, the God of his fathers, even if they aren’t clean according to the purification of the sanctuary.” Yahweh listened to Hezekiah, and healed the people.

Two Passovers?!

Did you know ...

There were two annual Passover days? One was on the 14th of the first month of the new year, and the second was on the 14th of the second month, reserved for those who had special needs preventing them from celebrating the official day in the first month.
WEB Num 9:2 “Let the children of Israel keep the Passover in its appointed season. On the fourteenth day of this month, at evening, you shall keep it in its appointed season. You shall keep it according to all its statutes and according to all its ordinances.” ... They kept the Passover in the first month, on the fourteenth day of the month at evening, in the wilderness of Sinai. According to all that Yahweh commanded Moses, so the children of Israel did. ... Yahweh spoke to Moses, saying, “Say to the children of Israel, ‘If any man of you or of your generations is unclean by reason of a dead body, or is on a journey far away, he shall still keep the Passover to Yahweh. In the second month on the fourteenth day at evening they shall keep it; they shall eat it with unleavened bread and bitter herbs. They shall leave none of it until the morning, nor break a bone of it. According to all the statute of the Passover they shall keep it. But the man who is clean, and is not on a journey, and fails to keep the Passover, that soul shall be cut off from his people. Because he didn’t offer the offering of Yahweh in its appointed season, that man shall bear his sin.
It might be noted that the restoration of the Passover by Hezekiah was kept on the second Passover, and God approved:
WEB 2 Chron 30:2 For the king had taken counsel with his princes and all the assembly in Jerusalem to keep the Passover in the second month. For they could not keep it at that [1st month] time, because the priests had not sanctified themselves in sufficient number, and the people had not gathered themselves together to Jerusalem. The thing was right in the eyes of the king and of all the assembly. ... For Hezekiah had prayed for them, saying, “May the good Yahweh pardon everyone who sets his heart to seek God, Yahweh, the God of his fathers, even if they aren’t clean according to the purification of the sanctuary.” Yahweh listened to Hezekiah, and healed the people.
(It might also be noted that doing "what was right in their own eyes" does not necessarily refer to doing wrong.) Hezekiah's great-grandson, however, did it right:
WEB 2 Chron 35:1 Josiah kept a Passover to Yahweh in Jerusalem. They killed the Passover on the fourteenth day of the first month.

God Seems to Like Beer. Huh, who knew?

Did you know... Yahweh commanded that the Israelites offer "strong drink" ("strong wine" - KJV; "beer" - HCSB) to him twice a day, once in the morning and once in the evening?
WEB Numbers 28:1 Yahweh spoke to Moses, saying, ... ‘This is the offering made by fire which you shall offer to Yahweh: male lambs a year old without defect, two day by day, for a continual burnt offering. You shall offer the one lamb in the morning, and you shall offer the other lamb at evening ... You shall pour out a drink offering of strong drink to Yahweh in the holy place. ... As the meal offering of the morning, and as its drink offering, you shall offer it, an offering made by fire, for a pleasant aroma to Yahweh.

God Can Speak Through "Pagans"

Did you know ... God spoke through the mouth of a "pagan", to a good righteous King of Judah, who foolishly didn't listen.
WEB 2 Chron 35:22 Nevertheless Josiah would not turn his face from him, but disguised himself, that he might fight with him, and didn’t listen to the words of Neco from the mouth of God, and came to fight in the valley of Megiddo.