Notice: Due to size constraints and loading performance considerations, scripts referenced in blog posts are not attached directly. To request access, please complete the following form: Script Request Form Note: A Google account is required to access the form.
Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

1Password Dynamic User Access Notification

During routine security audits I discovered that some users in our 1Password Business instance who had accepted invitations and appeared as active users but had no actual access to company vaults. These users were consuming paid licenses without being able to use 1Password for its intended purpose.

Visual Results : The Final Email

These are some images of the final email that the user will receive:



Then it will end with next steps for the user:


User State Analysis

When running 1Password CLI audits, we found users with this pattern, getting this data was covered in the post here - this wqould leave you with a output like this:

Name,Email,ID,State,Groups,DirectVaults,GroupCount,VaultCount
"John Doe","james.bond@croucher.cloud","<hex-data>","ACTIVE","Team Members","No direct vaults","1","0"

What this means:

  • User has accepted their 1Password invitation (State: ACTIVE)
  • User is assigned to the default "Team Members" group (GroupCount: 1)
  • User has zero vault assignments (VaultCount: 0)
  • User cannot access any company passwords or data
  • User is consuming a paid license unnecessarily

Why does this occur?

This typically occurs when:

  1. User is invited to 1Password
  2. Users receive and accept 1Password invitations
  3. Owner/Administrator never assigns users to appropriate vaults
  4. Users appear "active" but have no functional access
  5. License costs accumulate for non-functional accounts

Fixing these ghost users with Powershell and emails

I therefore created a PowerShell script that:

  1. Reads the latest 1Password user report CSV export
  2. Identifies users with vault access issues (VaultCount = 0)
  3. Sends automated email notifications to affected users
  4. Gives users 14 days to contact the security team
  5. Creates detailed logs for audit purposes

Email Notification

The reads the CSV and then generates HTML emails containing:

  • User's name and account details
  • Explanation of the access issue
  • 14-day timeline for resolution
  • Contact information for the security team
  • Professional formatting for corporate communication

Automatic File Processing

  • Automatically finds the latest CSV file matching "1password-users-report*.csv"
  • Handles various CSV formats and data types
  • Provides error handling for missing or corrupt files

Dynamic Email Generation

  • HTML emails with CSS styling for professional appearance
  • Personalized content using user data from CSV
  • Mobile-responsive design
  • Corporate branding appropriate for internal communications

Logging

All activities are logged with timestamps:

  • Users processed
  • Email notifications sent
  • Errors encountered
  • Summary statistics

Test Mode - Simulate who would get an email

The script includes a test mode for validation:

.\remediate-email-notification.ps1 -TestMode

This shows what emails would be sent without actually sending them.

Script : remediate-email-notification.ps1

This does not include the HTML content, ensure you add that in the bold section where you see the <html content goes here> section, also update the other bold sections are required.

# 1Password User Access Notification Script
# This script processes 1Password user reports and sends notifications to users without proper access

param(
    [string]$SmtpServer = "smtp.bear.local",
    [string]$SmtpPort = "25",
    [string]$FromEmail = "1password@croucher.cloud",
    [string]$SmtpUsername = "",
    [string]$SmtpPassword = "",
    [switch]$UseSSL = $false,
    [switch]$TestMode = $false
)

# Function to write to log file
function Write-LogEntry {
    param(
        [string]$Message,
        [string]$Level = "INFO"
    )
    
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "[$timestamp] [$Level] $Message"
    Write-Host $logEntry
    Add-Content -Path $script:LogFile -Value $logEntry
}

# Function to extract first and last name from full name
function Get-NameParts {
    param([string]$FullName)
    
    $nameParts = $FullName.Trim() -split '\s+'
    if ($nameParts.Count -ge 2) {
        $firstName = $nameParts[0]
        $lastName = $nameParts[-1]
    } else {
        $firstName = $nameParts[0]
        $lastName = ""
    }
    
    return @{
        FirstName = $firstName
        LastName = $lastName
    }
}

# Function to send email notification
function Send-AccessNotification {
    param(
        [string]$UserName,
        [string]$UserEmail,
        [string]$UserState,
        [int]$GroupCount,
        [int]$VaultCount
    )
    
    try {
        $nameparts = Get-NameParts -FullName $UserName
        $firstName = $nameparts.FirstName
        $lastName = $nameparts.LastName
        
        # Calculate the deletion date (14 days from today)
        $deletionDate = (Get-Date).AddDays(14).ToString("dddd, MMMM dd, yyyy")
        
        $subject = "Action Required: 1Password Account Access - Account Deletion Notice"
        
        $body = @"
<!DOCTYPE html>
<html lang="en">
<html content goes here>
</html>
"@

        if ($TestMode) {
            Write-LogEntry "TEST MODE: Would send email to $UserEmail with subject: $subject" "TEST"
            Write-LogEntry "TEST MODE: Email body preview (first 200 chars): $($body.Substring(0, [Math]::Min(200, $body.Length)))..." "TEST"
        } else {
            # Configure email parameters
            $emailParams = @{
                To = $UserEmail
                From = $FromEmail
                Subject = $subject
                Body = $body
                BodyAsHtml = $true
                SmtpServer = $SmtpServer
                Port = $SmtpPort
            }
            
            # Add SSL if specified
            if ($UseSSL) {
                $emailParams.Add('UseSsl', $true)
            }
            
            # Add credentials only if username and password are provided
            if ($SmtpUsername -and $SmtpPassword) {
                $securePassword = ConvertTo-SecureString $SmtpPassword -AsPlainText -Force
                $credential = New-Object System.Management.Automation.PSCredential($SmtpUsername, $securePassword)
                $emailParams.Add('Credential', $credential)
                Write-LogEntry "Using SMTP authentication for $SmtpServer" "INFO"
            } else {
                Write-LogEntry "Using SMTP without authentication for $SmtpServer" "INFO"
            }
            
            # Send email
            Send-MailMessage @emailParams
            
            Write-LogEntry "Email sent successfully to $UserEmail" "SUCCESS"
        }
        
        return $true
    }
    catch {
        Write-LogEntry "Failed to send email to $UserEmail. Error: $($_.Exception.Message)" "ERROR"
        return $false
    }
}

# Main script execution starts here
try {
    # Set up logging
    $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
    $script:LogFile = "1password_notification_log_$timestamp.txt"
    
    Write-LogEntry "Starting 1Password User Access Notification Script" "INFO"
    Write-LogEntry "Test Mode: $TestMode" "INFO"
    
    # Find the latest 1password-users-report CSV file
    $reportFiles = Get-ChildItem -Path "." -Filter "1password-users-report*.csv" | Sort-Object Name -Descending
    
    if ($reportFiles.Count -eq 0) {
        Write-LogEntry "No 1password-users-report CSV files found in current directory" "ERROR"
        # List all CSV files for debugging
        $allCsvFiles = Get-ChildItem -Path "." -Filter "*.csv"
        Write-LogEntry "Available CSV files in directory: $($allCsvFiles.Name -join ', ')" "INFO"
        exit 1
    }
    
    $latestFile = $reportFiles[0].Name
    Write-LogEntry "Found latest report file: $latestFile" "INFO"
    
    # Read and parse the CSV file
    $csvData = Import-Csv -Path $latestFile
    Write-LogEntry "Successfully imported $($csvData.Count) user records from CSV" "INFO"
    
    # Initialize counters
    $totalUsers = 0
    $usersNeedingNotification = 0
    $emailsSent = 0
    $emailsFailed = 0
    
    # Process each user record
    foreach ($user in $csvData) {
        $totalUsers++
        
        # Convert counts to integers (handle potential string values)
        $groupCount = [int]$user.GroupCount
        $vaultCount = [int]$user.VaultCount
        
        Write-LogEntry "Processing user: $($user.Name) - Groups: $groupCount, Vaults: $vaultCount" "INFO"
        
        # Check if user needs notification
        # Criteria: Users with no vault access (VaultCount = 0)
        # Users need vault access to access company data, regardless of group membership
        $needsNotification = ($vaultCount -eq 0)
        
        # Alternative criteria if you want to notify only users with BOTH no groups AND no vaults:
        # $needsNotification = ($groupCount -eq 0 -and $vaultCount -eq 0)
        
        if ($needsNotification) {
            $usersNeedingNotification++
            Write-LogEntry "User $($user.Name) requires notification (Groups: $groupCount, Vaults: $vaultCount)" "WARNING"
            
            # Send notification email
            $emailResult = Send-AccessNotification -UserName $user.Name -UserEmail $user.Email -UserState $user.State -GroupCount $groupCount -VaultCount $vaultCount
            
            if ($emailResult) {
                $emailsSent++
            } else {
                $emailsFailed++
            }
        } else {
            Write-LogEntry "User $($user.Name) has adequate access (Groups: $groupCount, Vaults: $vaultCount)" "INFO"
        }
    }
    
    # Generate summary report
    Write-LogEntry "=== PROCESSING SUMMARY ===" "INFO"
    Write-LogEntry "Total users processed: $totalUsers" "INFO"
    Write-LogEntry "Users needing notification: $usersNeedingNotification" "INFO"
    Write-LogEntry "Emails sent successfully: $emailsSent" "INFO"
    Write-LogEntry "Email failures: $emailsFailed" "INFO"
    Write-LogEntry "Log file created: $script:LogFile" "INFO"
    
    if ($TestMode) {
        Write-LogEntry "Script completed in TEST MODE - no actual emails were sent" "INFO"
    } else {
        Write-LogEntry "Script completed successfully" "INFO"
    }
    
} catch {
    Write-LogEntry "Script execution failed: $($_.Exception.Message)" "ERROR"
    Write-LogEntry "Stack trace: $($_.Exception.StackTrace)" "ERROR"
    exit 1
}

# Example usage:
# .\1password-notification.ps1 -TestMode  # Test run without sending emails
# .\1password-notification.ps1 -SmtpServer "mail.severntrent.co.uk"  # Internal SMTP without auth
# .\1password-notification.ps1 -SmtpServer "smtp.office365.com" -SmtpPort 587 -UseSSL -SmtpUsername "your-email@severntrent.co.uk" -SmtpPassword "your-password"  # External SMTP with auth

Previous Post Next Post

نموذج الاتصال