Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to IDBridge are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions use
a calendar scheme `YY.M.D.build` (see [CONTRIBUTING.md](CONTRIBUTING.md#versioning--releases)).

## [26.9.25.0] - 2026-09-25

### Changed
- **The `IDBridge Sync` scheduled task imports IDBridge by name, so `Update-Module` is the
whole update.** `Register-IDBridgeScheduledTask` used to write the loaded module's
manifest path into the task, and for a Gallery install that path is the version folder:
`Update-Module` installed the new release beside it and the task kept running the old one
forever (seen at a district running 26.9.9.0 with 26.9.21.0 installed). The task now runs
`Import-Module IDBridge`, so every run loads the newest version installed for all users,
and no read grant on the module folder is needed any more. Registration refuses a
per-user install, which the gMSA can never see, with a message saying how to install for
all users; `-ModulePath` still pins the task to one manifest (and grants read on its
folder) for anyone who wants that. The update-available log line now says
`Update-Module IDBridge -Scope AllUsers`, the README and getting-started install with
`-Scope AllUsers` (elevated), and the AD bootstrap doc gains the update step. A task registered before this release
still pins its version: after updating, run `Register-IDBridgeScheduledTask -Enabled`
(with your `-IntervalMinutes`) once and it follows new releases on its own.

## [26.9.21.0] - 2026-09-21

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ Repo: <https://github.com/SCNetworksLLC/IDBridge>
## Quick start

```powershell
# Install from the PowerShell Gallery (Update-Module IDBridge to upgrade later)
Install-Module IDBridge -Scope CurrentUser
# Install from the PowerShell Gallery, elevated (Update-Module IDBridge -Scope AllUsers to upgrade later)
Install-Module IDBridge -Scope AllUsers

Import-Module IDBridge

Expand Down
8 changes: 6 additions & 2 deletions docs/ad-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ single-DC lab).
|------|-------|
| Install + verify the gMSA on this host | `Install-ADServiceAccount` / `Test-ADServiceAccount`. Right after the account is created (or this host newly allowed), stale Kerberos tickets make this fail — the function purges the computer's tickets (`klist -li 0x3e7 purge`) and retries once on its own; only if that also fails does it ask for a reboot. |
| Grant "Log on as a batch job" | `SeBatchLogonRight` in the local security policy (LSA API — no cmdlet exists), required to start a scheduled task. **GPO caveat:** if a GPO manages that right, the GPO's list overwrites the local grant on the next policy refresh — add the gMSA to the GPO instead. |
| Grant filesystem rights | Read on the module folder and the runtime root (config, plugins, vault); modify on `Logs`, `Exports`, `Data`. |
| Register the task | Every `-IntervalMinutes` (default 15, anchored to midnight so runs land on :00/:15/:30/:45), named `-TaskName` (default `IDBridge Sync`, replaced if present): `pwsh -NoProfile -NonInteractive -Command "Import-Module '<manifest>'; Invoke-IDBridge -RootPath '<root>'"` as the gMSA. A still-running run is never overlapped; a hung run is killed after 1 hour. **Created disabled** unless `-Enabled` is passed. |
| Grant filesystem rights | Read on the runtime root (config, plugins, vault) and, for a pinned `-ModulePath` only, on that module folder (an all-users install is already readable by every account); modify on `Logs`, `Exports`, `Data`. |
| Register the task | Every `-IntervalMinutes` (default 15, anchored to midnight so runs land on :00/:15/:30/:45), named `-TaskName` (default `IDBridge Sync`, replaced if present): `pwsh -NoProfile -NonInteractive -Command "Import-Module IDBridge; Invoke-IDBridge -RootPath '<root>'"` as the gMSA — by name, so every run loads the newest version installed for all users; a per-user install is refused, and `-ModulePath` pins the task to one manifest instead. A still-running run is never overlapped; a hung run is killed after 1 hour. **Created disabled** unless `-Enabled` is passed. |

Task-vs-task overlap is Task Scheduler's job (above); an **interactive run vs. the
task** is covered by `Invoke-IDBridge` itself, which takes a machine-wide single-run
Expand Down Expand Up @@ -100,6 +100,10 @@ The task is registered **disabled** so nothing runs before the config is reviewe
- **Change the cadence:** re-run `Register-IDBridgeScheduledTask -IntervalMinutes <n>` —
the task is replaced in place (pass `-Enabled` to keep it live, the replacement is
otherwise disabled again).
- **Updating IDBridge:** `Update-Module IDBridge -Scope AllUsers` (elevated) and nothing
else — the task imports IDBridge by name. A task registered before 26.9.25.0 imports one
pinned version: run `Register-IDBridgeScheduledTask -IntervalMinutes <n> -Enabled` once
after the update to stop pinning the old version.
- **Offboarding:** disable or delete the task and the gMSA
(`Remove-ADServiceAccount`); the OU ACEs name the account's SID and die with it
(remove them from the OU's Security tab at leisure).
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ overrides** (`-ReadOnly/-TestRun/-SkipADCheck/-TraceLogging/-SkipAD/-SkipGoogle/
(switches win over the config file; `-SkipADCheck` and `-SkipAD` are additionally
forwarded into `Initialize-IDBridge` itself so they land before the AD module import),
runs the **notify-only update check** (a newer Gallery release logs a `Warn`
to run `Update-Module IDBridge`; offline/blocked environments log a Trace skip — nothing
to run `Update-Module IDBridge -Scope AllUsers`; offline/blocked environments log a Trace skip — nothing
is ever installed), and **acquires Google auth** (if `GoogleToken.Enabled`) via
`Connect-IDBridgeGoogle`: reads the service-account key JSON from the secret vault
(`Get-IDBridgeSecret -Name 'GoogleAuth-ServiceAccount'`; **no file fallback**), validates
Expand Down
43 changes: 31 additions & 12 deletions docs/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,19 @@ sites). **Returns:** the gMSA.
Host-side follow-up: installs + verifies the gMSA on this computer (auto-purging the
computer's Kerberos tickets and retrying once when it was only just allowed), grants it
'Log on as a batch job' (`Grant-IDBridgeBatchLogonRight`) and the filesystem rights a
run needs (read on module + runtime root, modify on `Logs`/`Exports`/`Data`), and
registers a Task Scheduler task running `Invoke-IDBridge -RootPath <root>` in pwsh as
the gMSA every `-IntervalMinutes` (`-LogonType Password`, no stored credential; no
overlap, hung runs killed after 1 h). **Created disabled unless `-Enabled`** — verify,
then `Enable-ScheduledTask`. Idempotent — re-running replaces the task. Elevated
session. **Params:** `-AccountName`, `-TaskName` (def `IDBridge Sync`),
`-IntervalMinutes` (def 15, midnight-aligned), `-Enabled`, `-RootPath` (def
`Paths.Root`), `-ModulePath` (def the loaded module's manifest). **Returns:** the
registered task.
run needs (read on the runtime root and a pinned `-ModulePath`'s folder, modify on
`Logs`/`Exports`/`Data`), and registers a Task Scheduler task running
`Import-Module IDBridge; Invoke-IDBridge -RootPath <root>` in pwsh as the gMSA every
`-IntervalMinutes` (`-LogonType Password`, no stored credential; no overlap, hung runs
killed after 1 h). The import is by name (`Get-IDBridgeTaskCommand`), so every run loads
the newest version installed for all users and `Update-Module IDBridge -Scope AllUsers`
alone updates the task; a per-user install is refused (`Test-IDBridgeSharedModulePath`).
A task registered before 26.9.25.0 pins one version — re-register it once. **Created
disabled unless `-Enabled`** — verify, then `Enable-ScheduledTask`. Idempotent —
re-running replaces the task. Elevated session. **Params:** `-AccountName`, `-TaskName`
(def `IDBridge Sync`), `-IntervalMinutes` (def 15, midnight-aligned), `-Enabled`,
`-RootPath` (def `Paths.Root`), `-ModulePath` (pins the task to that manifest; def import
by name). **Returns:** the registered task.

### `Grant-IDBridgeBatchLogonRight` 🔒
**Params:** `-Identity` (mandatory account, e.g. `'DOMAIN\gMSA-IDBridge$'`). Grants
Expand All @@ -93,6 +97,20 @@ built-in P/Invoke wrapper over `advapi32.dll` (`LsaOpenPolicy`/`LsaAddAccountRig
no cmdlet exists for user rights). Idempotent; local only (a GPO managing the right
overwrites it on refresh); elevated session. No return.

### `Test-IDBridgeSharedModulePath` 🔒 🧮
**Params:** `-ModuleBase` (mandatory), `-SharedModulePaths` (def the machine-scope
`PSModulePath` plus `$env:ProgramFiles\PowerShell\Modules`, pwsh's all-users folder).
Whether the module folder sits under an all-users module path — case-insensitive, `\`
and `/` alike, trailing separators ignored, whole path segments only (`...\ModulesX` is
not under `...\Modules`). Decides between the import by name and the per-user refusal in
`Register-IDBridgeScheduledTask`. **Returns:** `[bool]`.

### `Get-IDBridgeTaskCommand` 🔒 🧮
**Params:** `-RootPath` (mandatory), `-ModulePath`. The full pwsh argument string the
scheduled task runs: `-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command
"Import-Module IDBridge; Invoke-IDBridge -RootPath '<root>'"`, or `Import-Module
'<ModulePath>'` in its place when pinned. **Returns:** `[string]`.

### `Get-IDBridgeConfig`
Accessor for `$script:IDBridgeConfig`. Throws if called before `Initialize-IDBridge`.

Expand Down Expand Up @@ -150,9 +168,10 @@ the Pulse dashboard. Delete the file when cloning a config to a new install.

### `Test-IDBridgeUpdateAvailable` 🔒 🌐
No params. Queries the PowerShell Gallery for the latest stable IDBridge release (10 s
timeout) and logs a `Warn` when a newer one exists ("run `Update-Module IDBridge`"), Trace
when current. Notify-only — never installs anything. Failures throw to the caller
(`Invoke-IDBridge` swallows them at Trace). **Returns:** `[bool]`.
timeout) and logs a `Warn` when a newer one exists ("run
`Update-Module IDBridge -Scope AllUsers`", and re-register a task from before 26.9.25.0
once), Trace when current. Notify-only — never installs anything. Failures throw to the
caller (`Invoke-IDBridge` swallows them at Trace). **Returns:** `[bool]`.

### `Get-IDBridgeTemplateVersion` 🔒
**Params:** `-Path`. Reads the `# TemplateVersion: <n>` marker from a template file.
Expand Down
8 changes: 6 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,24 @@ touching either directory. Step 9 turns writes on deliberately.
- **Windows** with **PowerShell 7.5+** (`pwsh`).
- **ActiveDirectory** module (RSAT) on a **domain-joined** machine — only if you're syncing
AD. Google-only deployments skip this (run with `-SkipAD`, or leave `AD.enabled = $false`).
- An **elevated** session for step 3 (the secret certificate goes in the machine store).
- An **elevated** session for steps 2 and 3 (the module installs for all users, the secret
certificate goes in the machine store).
- For step 4 (once, ever): sign-in as the district's **Google Workspace super admin**, and
*Google Cloud Platform* turned **ON** for that admin (Admin console → Apps → Additional
Google services). See [google-bootstrap.md](google-bootstrap.md#prerequisites).

## 2. Install and scaffold

```powershell
Install-Module IDBridge -Scope CurrentUser # Update-Module IDBridge to upgrade later
Install-Module IDBridge -Scope AllUsers # elevated; Update-Module IDBridge -Scope AllUsers to upgrade later
Import-Module IDBridge

Install-IDBridge # -RootPath 'D:\IDBridge' to relocate everything
```

Install for all users: the scheduled task runs as a gMSA, which only sees modules
installed for all users (a `-Scope CurrentUser` install lives in your own profile).

`Install-IDBridge` creates the runtime tree under `C:\IDBridge`
(`Config/Logs/Exports/Plugins/Data/Vault`), writes a default
`Config\IDBridgeConfig.psd1` — every feature disabled, all safety brakes on (`ReadOnly`,
Expand Down
2 changes: 1 addition & 1 deletion docs/google-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ tenant, signed in as their Google Workspace super admin**. Everything it creates
the district's Google Cloud organization — SC Networks retains nothing afterward.

```powershell
Install-Module IDBridge -Scope CurrentUser # from the PowerShell Gallery
Install-Module IDBridge -Scope AllUsers # from the PowerShell Gallery, elevated
Import-Module IDBridge
Initialize-IDBridge # loads config/paths/logging; Google auth happens at run time, not here
Initialize-IDBridgeGoogleServiceAccount -CreateProject
Expand Down
2 changes: 1 addition & 1 deletion src/IDBridge/IDBridge.psd1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
RootModule = 'IDBridge.psm1'
ModuleVersion = '26.9.21.0'
ModuleVersion = '26.9.25.0'
GUID = 'a0a0c664-888e-44e2-9c12-fc8647a520c0'
Author = 'Sam Cattanach'
CompanyName = 'SC Networks LLC'
Expand Down
41 changes: 41 additions & 0 deletions src/IDBridge/Private/Get-IDBridgeTaskCommand.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<#
.SYNOPSIS
Build the pwsh argument string the IDBridge scheduled task runs (internal).

.DESCRIPTION
Internal helper for Register-IDBridgeScheduledTask: returns the full -Argument text for the
task's pwsh action. By default the task imports IDBridge by name, so every run loads the
newest version installed for all users and Update-Module alone picks up a new release. With
-ModulePath the task imports that one manifest instead - an explicit pin. Uses no module
state or Write-Log.

.PARAMETER RootPath
Runtime root the task passes to Invoke-IDBridge -RootPath.

.PARAMETER ModulePath
Path to an IDBridge.psd1 manifest to pin the task to. Omit it to import IDBridge by name.

.OUTPUTS
[string] the pwsh argument string.

.EXAMPLE
Get-IDBridgeTaskCommand -RootPath 'C:\IDBridge'

.NOTES
Created by: Sam Cattanach
Modified: 2026-09-25
#>
function Get-IDBridgeTaskCommand {
[CmdletBinding()]
[OutputType([string])]
param (
[Parameter(Mandatory)]
[string]$RootPath,

[Parameter()]
[string]$ModulePath
)

$importCommand = if ($ModulePath) { "Import-Module '$ModulePath'" } else { "Import-Module IDBridge" }
return "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command `"$importCommand; Invoke-IDBridge -RootPath '$RootPath'`""
}
56 changes: 56 additions & 0 deletions src/IDBridge/Private/Test-IDBridgeSharedModulePath.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<#
.SYNOPSIS
Test whether a module folder sits under an all-users module path (internal).

.DESCRIPTION
Internal helper for Register-IDBridgeScheduledTask: returns $true when -ModuleBase is
under any of -SharedModulePaths, the module folders every account on the machine sees - so
the gMSA's pwsh finds the module by name. The comparison is case-insensitive, treats '\'
and '/' alike, ignores trailing separators, and matches whole path segments only
('...\PowerShell\ModulesX' is not under '...\PowerShell\Modules'). Uses no module state or
Write-Log.

.PARAMETER ModuleBase
The loaded module's folder, e.g. 'C:\Program Files\PowerShell\Modules\IDBridge\26.9.21.0'.

.PARAMETER SharedModulePaths
The all-users module folders. Defaults to the machine-scope PSModulePath plus
$env:ProgramFiles\PowerShell\Modules - pwsh's all-users folder, where
Install-Module -Scope AllUsers puts a module; pwsh adds it at startup and it is not in the
machine variable.

.OUTPUTS
[bool] $true when ModuleBase is under a shared module path, otherwise $false.

.EXAMPLE
Test-IDBridgeSharedModulePath -ModuleBase (Get-Module IDBridge).ModuleBase

.NOTES
Created by: Sam Cattanach
Modified: 2026-09-25
#>
function Test-IDBridgeSharedModulePath {
[CmdletBinding()]
[OutputType([bool])]
param (
[Parameter(Mandatory)]
[string]$ModuleBase,

[Parameter()]
[string[]]$SharedModulePaths
)

if (-not $PSBoundParameters.ContainsKey('SharedModulePaths')) {
$SharedModulePaths = [Environment]::GetEnvironmentVariable('PSModulePath', 'Machine') -split [IO.Path]::PathSeparator
if ($env:ProgramFiles) { $SharedModulePaths += Join-Path $env:ProgramFiles 'PowerShell\Modules' }
}

$base = $ModuleBase.Replace('\', '/').TrimEnd('/')
foreach ($sharedPath in $SharedModulePaths | Where-Object { $_ }) {
$shared = $sharedPath.Replace('\', '/').TrimEnd('/')
if ($base -eq $shared -or $base.StartsWith("$shared/", [StringComparison]::OrdinalIgnoreCase)) {
return $true
}
}
return $false
}
7 changes: 4 additions & 3 deletions src/IDBridge/Private/Test-IDBridgeUpdateAvailable.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Check the PowerShell Gallery for a newer IDBridge release and log a warning if o
.DESCRIPTION
Internal helper: queries the PowerShell Gallery for the latest stable IDBridge version
(prereleases excluded) and compares it to the running module version. A newer release logs
a Warn ("Update available... run Update-Module IDBridge") so it surfaces in the log file,
a Warn ("Update available... Run Update-Module IDBridge -Scope AllUsers", and the one-time
re-registration a task from before 26.9.25.0 needs) so it surfaces in the log file,
console, and Google Sheet log; an up-to-date install logs at Trace. Notify-only — nothing
is ever downloaded or installed. The gallery call has a 10 second timeout and any failure
throws to the caller, which is expected to swallow it (offline or gallery-blocked
Expand All @@ -19,7 +20,7 @@ try { Test-IDBridgeUpdateAvailable | Out-Null } catch { }

.NOTES
Created by: Sam Cattanach
Modified: 2026-07-13
Modified: 2026-09-25
#>
function Test-IDBridgeUpdateAvailable {
[CmdletBinding()]
Expand All @@ -34,7 +35,7 @@ function Test-IDBridgeUpdateAvailable {
$latestVersion = [version](@($response)[0].properties.Version)

if ($latestVersion -gt $installedVersion) {
Write-Log -Message "Update available: IDBridge $latestVersion is on the PowerShell Gallery (installed: $installedVersion). Run Update-Module IDBridge, then restart the session." -Level Warn
Write-Log -Message "Update available: IDBridge $latestVersion is on the PowerShell Gallery (installed: $installedVersion). Run Update-Module IDBridge -Scope AllUsers (elevated), then restart the session. A task registered before 26.9.25.0 imports one pinned version - run Register-IDBridgeScheduledTask -Enabled once after updating so it follows new releases on its own." -Level Warn
return $true
}

Expand Down
Loading
Loading