mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-08 16:21:38 +00:00
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28b909a686 | ||
|
|
ce73c62e81 | ||
|
|
8e71e18aaa | ||
|
|
d78506fa9b | ||
|
|
a2b226a124 | ||
|
|
8412ba78b2 | ||
|
|
9d01e3fa27 | ||
|
|
9a981bf02e | ||
|
|
12667bf898 | ||
|
|
7e664b437d | ||
|
|
ad3d4e9a57 | ||
|
|
4f992f4d41 | ||
|
|
a987ff4eac | ||
|
|
05fd22565d | ||
|
|
8470949e82 | ||
|
|
f56bf838ad | ||
|
|
33d808ff4b | ||
|
|
35fde0e48b | ||
|
|
7c5e79f309 | ||
|
|
71bac7c2eb | ||
|
|
539b91a27e | ||
|
|
d71bf9b6cb | ||
|
|
0c2fdeee6b | ||
|
|
33df3132b8 | ||
|
|
118e1e3ff5 | ||
|
|
2fdab61fc8 | ||
|
|
a883bf721f | ||
|
|
ca20bcbda0 | ||
|
|
a927dda9aa | ||
|
|
a2d80cac46 | ||
|
|
fcf2712f3f | ||
|
|
8dc2b4bd64 | ||
|
|
679019fc3c | ||
|
|
c4da8110a3 | ||
|
|
7bcd0611c2 | ||
|
|
485cbb65bb | ||
|
|
29a2e224bc | ||
|
|
ac25dd6557 | ||
|
|
673effa9f8 | ||
|
|
a936a5fa3d | ||
|
|
a1b073cbdd | ||
|
|
093015c617 | ||
|
|
2bee44f7e9 | ||
|
|
645155a2ea | ||
|
|
da2298ae23 | ||
|
|
02188e9c49 | ||
|
|
28b5ab34c3 | ||
|
|
b338e2da2f | ||
|
|
b1af32e487 | ||
|
|
a32cc146ef | ||
|
|
1ed1d8552b | ||
|
|
4f58f7c967 | ||
|
|
c41b94487e | ||
|
|
7f95020e6f | ||
|
|
9181a35c10 | ||
|
|
c95997e2d5 | ||
|
|
9348f57141 | ||
|
|
dab6272d55 | ||
|
|
f548d49e19 | ||
|
|
081965fa79 | ||
|
|
bcef6f9391 | ||
|
|
dfb4c88b69 | ||
|
|
f10fca04c9 | ||
|
|
096eef3f59 | ||
|
|
f9cd2f56d6 | ||
|
|
91559239ca | ||
|
|
a00256ee9f | ||
|
|
970697ec65 | ||
|
|
06d131ec55 | ||
|
|
094616e482 | ||
|
|
cbfae9226a | ||
|
|
c35cdcf4c3 | ||
|
|
d87ece177d | ||
|
|
41535666b6 | ||
|
|
20d1b18009 | ||
|
|
c27e48dd5c | ||
|
|
44fe8a22e0 | ||
|
|
21df315887 | ||
|
|
3a7c470e6e | ||
|
|
22feec5136 | ||
|
|
a30f8c325f | ||
|
|
0770c20992 | ||
|
|
cc7fb0df7b | ||
|
|
fec061c250 | ||
|
|
aef3b23061 | ||
|
|
3518fc8ad3 | ||
|
|
e5dab74336 | ||
|
|
3aef51781e | ||
|
|
23deb2526c |
133
.github/workflows/build.yml
vendored
133
.github/workflows/build.yml
vendored
@@ -126,15 +126,21 @@ jobs:
|
||||
name: Test Python 3.12
|
||||
- os: ubuntu-24.04
|
||||
goal: test
|
||||
python: "3.15-dev"
|
||||
python: "3.13"
|
||||
freethreaded: false
|
||||
jid: 18
|
||||
name: Test Python 3.13
|
||||
- os: ubuntu-24.04
|
||||
goal: test
|
||||
python: "3.15-dev"
|
||||
freethreaded: false
|
||||
jid: 19
|
||||
name: Test Python 3.15-dev
|
||||
- os: ubuntu-24.04
|
||||
goal: test
|
||||
python: "3.14"
|
||||
freethreaded: true
|
||||
jid: 19
|
||||
jid: 20
|
||||
name: Test Python 3.14 freethread
|
||||
|
||||
steps:
|
||||
@@ -256,7 +262,7 @@ jobs:
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
|
||||
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
|
||||
|
||||
|
||||
- name: Windows Configure VCode
|
||||
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
|
||||
if: runner.os == 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
|
||||
@@ -296,8 +302,9 @@ jobs:
|
||||
fi
|
||||
PYEXTERNALS_PATH=$(cygpath -u "${PYTHON_SOURCE_PATH}/PCbuild/${PYEXTERNALS_ARCH}")
|
||||
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${PYEXTERNALS_PATH}"
|
||||
echo "PYTHON=${PYTHON_SOURCE_PATH}/PCbuild/${PYEXTERNALS_ARCH}/python.exe" >> $GITHUB_ENV
|
||||
echo "PYTHON=${PYTHON_INSTALL_PATH}\python.exe" >> $GITHUB_ENV
|
||||
echo "WIX_ARCH=${WIX_ARCH}" >> $GITHUB_ENV
|
||||
echo "PS_PYTHON_INSTALL_PATH=$(cygpath -w $PYTHON_INSTALL_PATH)" >> $GITHUB_ENV
|
||||
fi
|
||||
echo "We'll run make with: ${MAKEOPT}"
|
||||
echo "staticx=${staticx}" >> $GITHUB_ENV
|
||||
@@ -440,7 +447,7 @@ jobs:
|
||||
pip install --upgrade sphinx
|
||||
sphinx-build --version
|
||||
|
||||
- name: Windows Config/Build Python
|
||||
- name: Windows Config/Build/Install Python
|
||||
if: matrix.goal == 'build' && runner.os == 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
|
||||
shell: powershell
|
||||
run: |
|
||||
@@ -449,13 +456,16 @@ jobs:
|
||||
Copy-Item -Path "${env:GITHUB_WORKSPACE}\src\tools\openssl.props" -Destination PCBuild\ -Verbose
|
||||
if (${env:RUNNER_ARCH} -eq "X64") {
|
||||
$env:arch = "x64"
|
||||
PCBuild\build.bat -c Release -p $env:arch --pgo
|
||||
#PCBuild\build.bat -c Release -p $env:arch --pgo
|
||||
} elseif (${env:RUNNER_ARCH} -eq "ARM64") {
|
||||
$env:arch = "ARM64"
|
||||
# TODO: figure out why Windows ARM64 isn't compat with PGO optimiazation
|
||||
# causes 10-20% slowdown in Python
|
||||
PCBuild\build.bat -c Release -p $env:arch
|
||||
#PCBuild\build.bat -c Release -p $env:arch
|
||||
}
|
||||
PCBuild\build.bat -c Release -p $env:arch --pgo
|
||||
.\python.bat PC\layout --precompile --preset-default --copy $env:PS_PYTHON_INSTALL_PATH
|
||||
Get-ChildItem -Path $env:PS_PYTHON_INSTALL_PATH -File
|
||||
|
||||
- name: Mac/Linux Build Python
|
||||
if: matrix.goal == 'build' && runner.os != 'Windows' && steps.cache-python-ssl.outputs.cache-hit != 'true'
|
||||
@@ -661,28 +671,27 @@ jobs:
|
||||
echo "GAM Version ${GAMVERSION}"
|
||||
echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Install WinAppDriver
|
||||
- name: Initialize Windows Desktop Shell
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Checking for Windows Explorer shell..."
|
||||
if (-not (Get-Process -Name explorer -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "Explorer not found. Booting the desktop shell..."
|
||||
Start-Process explorer.exe
|
||||
# Give the desktop a few seconds to fully render the taskbar
|
||||
Start-Sleep -Seconds 10
|
||||
} else {
|
||||
Write-Host "Explorer is already running."
|
||||
}
|
||||
|
||||
- name: Install NPM deps
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
choco install -y winappdriver
|
||||
|
||||
- name: Enabled dev mode for WinAppDriver
|
||||
if: runner.os == 'Windows'
|
||||
shell: cmd
|
||||
run : |
|
||||
reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
|
||||
|
||||
- name: Install appium and totp tools
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
echo "Installing appium..."
|
||||
npm install -g appium
|
||||
#echo "Installing appium..."
|
||||
#npm install -g appium
|
||||
echo "Installing totp-generator..."
|
||||
npm install "totp-generator"
|
||||
echo "Installing wdio..."
|
||||
npm install @wdio/cli
|
||||
echo "Installing appium win driver..."
|
||||
appium driver install windows
|
||||
npm install totp-generator
|
||||
|
||||
- name: Install Certum MSI
|
||||
if: runner.os == 'Windows'
|
||||
@@ -704,18 +713,21 @@ jobs:
|
||||
env:
|
||||
TOTP_SECRET: ${{ secrets.TOTP_SECRET }}
|
||||
run: |
|
||||
# disable win private firewall that interferes with appium server
|
||||
Set-NetFirewallProfile -Profile Private -Enabled False
|
||||
$appiumCmd = Get-Command appium
|
||||
$appiumPath = $appiumCmd.Path
|
||||
Start-Process -Filepath "powershell.exe" -ArgumentList "-File", $appiumPath, "--address", "127.0.0.1", "--log-level", "error"
|
||||
Start-Sleep -Seconds 10
|
||||
write-host "appium started"
|
||||
write-host "running SimplySignDesktop login..."
|
||||
node tools/ssd.mjs --log-level warn
|
||||
write-host "sleeping during login..."
|
||||
Start-Sleep 10
|
||||
|
||||
- name: Archive png artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
|
||||
if: runner.os == 'Windows'
|
||||
with:
|
||||
archive: true
|
||||
name: images-${{ matrix.os }}
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
*.png
|
||||
|
||||
- name: Sign gam.exe
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
@@ -758,11 +770,6 @@ jobs:
|
||||
echo "GAM Archive ${GAM_ARCHIVE}"
|
||||
tar -C "${gampath}/.." --create --verbose --exclude-from "${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" --file $GAM_ARCHIVE --xz gam7
|
||||
|
||||
- name: Install Wix on Win ARM64
|
||||
if: runner.os == 'Windows' && runner.arch == 'ARM64'
|
||||
run: |
|
||||
choco install wixtoolset
|
||||
|
||||
- name: Windows package zip
|
||||
if: runner.os == 'Windows' && matrix.goal != 'test'
|
||||
run: |
|
||||
@@ -772,33 +779,15 @@ jobs:
|
||||
GAM_ARCHIVE="${GITHUB_WORKSPACE}/gam-${GAMVERSION}-windows-${arch}.zip"
|
||||
/c/Program\ Files/7-Zip/7z.exe a -tzip "$GAM_ARCHIVE" gam7 "-xr@${GITHUB_WORKSPACE}/.github/actions/package_exclusions.txt" -bb3
|
||||
|
||||
- name: Windows package MSI
|
||||
- name: Windows package exe with Inno Setup
|
||||
if: runner.os == 'Windows' && matrix.goal != 'test'
|
||||
run: |
|
||||
export MSI_FILENAME="${GITHUB_WORKSPACE}/gam-${GAMVERSION}-windows-${arch}.msi"
|
||||
# auto-generate a lib.wxs based on the files PyInstaller created for the lib/ directory
|
||||
/c/Program\ Files\ \(x86\)/WiX\ Toolset\ v3.14/bin/heat.exe dir "${gampath}/lib" -ke -srd -cg Lib -gg -dr lib -directoryid lib -out lib.wxs
|
||||
$PYTHON tools/gen-wix-xml-filelist.py lib.wxs
|
||||
echo "-- begin lib.wxs --"
|
||||
cat lib.wxs
|
||||
echo "-- end lib.wxs --"
|
||||
/c/Program\ Files\ \(x86\)/WiX\ Toolset\ v3.14/bin/candle.exe -arch "${WIX_ARCH}" gam.wxs lib.wxs
|
||||
/c/Program\ Files\ \(x86\)/WiX\ Toolset\ v3.14/bin/light.exe -ext /c/Program\ Files\ \(x86\)/WiX\ Toolset\ v3.14/bin/WixUIExtension.dll gam.wixobj lib.wixobj -b "${gampath}/lib" -o "$MSI_FILENAME" || true;
|
||||
rm -v -f *.wixpdb
|
||||
rm -v -f *.wixobj
|
||||
echo "MSI_FILENAME=${MSI_FILENAME}" >> $GITHUB_ENV
|
||||
|
||||
- name: Sign GAM MSI
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
write-Host "Signing ${env:MSI_FILENAME}...."
|
||||
# Always explicitely use x64 version os signtool.exe, arm64 version apparently can't
|
||||
# see Certum certs since SimplySignDesktop is x64-only today.
|
||||
Start-Process -Wait -NoNewWindow -ErrorAction Continue -FilePath 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe' -ArgumentList "sign", "/sha1", "$env:WINDOWS_CODESIGN_CERT_HASH", "/tr", "http://time.certum.pl", "/td", "SHA256", "/fd", "SHA256", "/v", "$env:MSI_FILENAME"
|
||||
write-Host "Verifying signature of ${env:MSI_FILENAME}...."
|
||||
# verify signature. If we failed to sign we should fail to verify and die.
|
||||
& 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe' verify /pa /v "$env:MSI_FILENAME"
|
||||
choco install innosetup
|
||||
export signtool="C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe"
|
||||
iscc \
|
||||
/S"gamsigntool=$signtool sign /sha1 $WINDOWS_CODESIGN_CERT_HASH /tr http://time.certum.pl /td SHA256 /fd SHA256 /v \$f" \
|
||||
/O"$GITHUB_WORKSPACE" \
|
||||
gam.iss
|
||||
|
||||
- name: Attest that gam package files were generated from this Action
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
@@ -807,7 +796,8 @@ jobs:
|
||||
subject-path: |
|
||||
gam*.tar.xz
|
||||
gam*.zip
|
||||
gam*.msi
|
||||
gam*.exe
|
||||
# gam*.msi
|
||||
|
||||
- name: Archive tar.xz artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
|
||||
@@ -827,14 +817,14 @@ jobs:
|
||||
path: |
|
||||
gam*.zip
|
||||
|
||||
- name: Archive msi artifacts
|
||||
- name: Archive exe artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
|
||||
if: runner.os == 'Windows'
|
||||
with:
|
||||
archive: false
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
gam*.msi
|
||||
gam*.exe
|
||||
|
||||
- name: Basic Tests build jobs only
|
||||
if: matrix.goal != 'test' && steps.cache-python-ssl.outputs.cache-hit != 'true'
|
||||
@@ -1111,11 +1101,12 @@ jobs:
|
||||
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit != 'true'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
if [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
tar_folders="src/cpython/ bin/ssl"
|
||||
else
|
||||
tar_folders="bin/"
|
||||
fi
|
||||
#if [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
# tar_folders="src/cpython/ bin/ssl"
|
||||
#else
|
||||
# tar_folders="bin/"
|
||||
#fi
|
||||
tar_folders="bin/"
|
||||
echo '.git*' > ./excludes.txt
|
||||
tar cJvvf cache.tar.xz --exclude-from=excludes.txt $tar_folders
|
||||
|
||||
|
||||
59
.github/workflows/get-cacerts.yml
vendored
59
.github/workflows/get-cacerts.yml
vendored
@@ -1,14 +1,9 @@
|
||||
name: Check for Google Root CA Updates
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'wiki/**'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'wiki/**'
|
||||
schedule:
|
||||
- cron: '23 23 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -17,9 +12,9 @@ defaults:
|
||||
|
||||
jobs:
|
||||
check-certs:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token
|
||||
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
|
||||
@@ -30,9 +25,51 @@ jobs:
|
||||
echo "Current hash is: ${CURRENT_HASH}"
|
||||
echo "CURRENT_HASH=${CURRENT_HASH}" >> $GITHUB_ENV
|
||||
|
||||
- name: Get latest cacerts.pem file from Google
|
||||
- name: Generate GAM-specific bundle with LE + Google roots
|
||||
run: |
|
||||
curl -o ./cacerts.pem -vvvv https://pki.goog/roots.pem
|
||||
OUTPUT_FILE="cacerts.pem"
|
||||
> "$OUTPUT_FILE"
|
||||
|
||||
process_cert() {
|
||||
local url="$1"
|
||||
local op_ca="$2"
|
||||
local label="$3"
|
||||
local tmp_cert=$(mktemp)
|
||||
curl "$url" > "$tmp_cert"
|
||||
local issuer=$(openssl x509 -noout -issuer -in "$tmp_cert" | sed -e 's/^issuer= *//')
|
||||
local subject=$(openssl x509 -noout -subject -in "$tmp_cert" | sed -e 's/^subject= *//')
|
||||
local serial_hex=$(openssl x509 -noout -serial -in "$tmp_cert" | sed -e 's/^serial=//')
|
||||
local serial_dec=$(python3 -c "print(int('$serial_hex', 16))")
|
||||
local md5=$(openssl x509 -noout -fingerprint -md5 -in "$tmp_cert" | sed -e 's/.*=//' | tr '[:upper:]' '[:lower:]')
|
||||
local sha1=$(openssl x509 -noout -fingerprint -sha1 -in "$tmp_cert" | sed -e 's/.*=//' | tr '[:upper:]' '[:lower:]')
|
||||
local sha256=$(openssl x509 -noout -fingerprint -sha256 -in "$tmp_cert" | sed -e 's/.*=//' | tr '[:upper:]' '[:lower:]')
|
||||
echo "# Operating CA: $op_ca" >> "$OUTPUT_FILE"
|
||||
echo "# Issuer: $issuer" >> "$OUTPUT_FILE"
|
||||
echo "# Subject: $subject" >> "$OUTPUT_FILE"
|
||||
echo "# Label: \"$label\"" >> "$OUTPUT_FILE"
|
||||
echo "# Serial: $serial_dec" >> "$OUTPUT_FILE"
|
||||
echo "# MD5 Fingerprint: $md5" >> "$OUTPUT_FILE"
|
||||
echo "# SHA1 Fingerprint: $sha1" >> "$OUTPUT_FILE"
|
||||
echo "# SHA256 Fingerprint: $sha256" >> "$OUTPUT_FILE"
|
||||
cat "$tmp_cert" >> "$OUTPUT_FILE"
|
||||
echo "" >> "$OUTPUT_FILE"
|
||||
rm "$tmp_cert"
|
||||
}
|
||||
|
||||
echo "#" >> "$OUTPUT_FILE"
|
||||
echo "# This is a custom certificate authority bundle for GAM" >> "$OUTPUT_FILE"
|
||||
echo "# It's composed of Let's Encrypt Root CAs and Google's" >> "$OUTPUT_FILE"
|
||||
echo "# certificate bundle. This should be the minimal list of" >> "$OUTPUT_FILE"
|
||||
echo "# CAs required to talk to Google and Github." >> "$OUTPUT_FILE"
|
||||
echo"" >> "$OUTPUT_FILE"
|
||||
echo "Processing Let's Encrypt ISRG Root X1..."
|
||||
process_cert "https://letsencrypt.org/certs/isrgrootx1.pem" "Let's Encrypt" "ISRG Root X1"
|
||||
echo "Processing Let's Encrypt ISRG Root X2..."
|
||||
process_cert "https://letsencrypt.org/certs/isrg-root-x2.pem" "Let's Encrypt" "ISRG Root X2"
|
||||
|
||||
echo "Appending Google's roots.pem..."
|
||||
curl -s https://pki.goog/roots.pem >> "$OUTPUT_FILE"
|
||||
echo "Done! The new bundle has been saved to $OUTPUT_FILE."
|
||||
|
||||
- name: Compare hashes
|
||||
run: |
|
||||
@@ -51,6 +88,6 @@ jobs:
|
||||
git diff --quiet && git diff --staged --quiet || git commit -am '[ci skip] Updated cacerts.pem'
|
||||
|
||||
- name: Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
uses: ad-m/github-push-action@77c5b412c50b723d2a4fbc6d71fb5723bcd439aa
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -4335,14 +4335,14 @@ gam update deviceuserstate <DeviceUserEntity> [clientid <String>]
|
||||
# Cloud Identity Policies
|
||||
|
||||
gam info policies <CIPolicyNameEntity>
|
||||
[nowarnings] [noappnames]
|
||||
[nowarnings] [noappnames] [noidmappimg]
|
||||
[formatjson]
|
||||
gam print policies [todrive <ToDriveAttribute>*]
|
||||
[filter <String>] [nowarnings] [noappnames]
|
||||
[filter <String>] [nowarnings] [noappnames] [noidmappimg]
|
||||
[group <REMatchPattern>] [ou|org|orgunit <REMatchPattern>]
|
||||
[formatjson [quotechar <Character>]]
|
||||
gam show policies
|
||||
[filter <String>] [nowarnings] [noappnames]
|
||||
[filter <String>] [nowarnings] [noappnames] [noidmappimg]
|
||||
[group <REMatchPattern>] [ou|org|orgunit <REMatchPattern>]
|
||||
[formatjson]
|
||||
|
||||
@@ -4926,36 +4926,36 @@ gam print schema|schemas [todrive <ToDriveAttribute>*]
|
||||
gam sendemail [recipient|to] <RecipientEntity>
|
||||
[from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
|
||||
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
||||
[subject <String>]
|
||||
[<MessageContent>]
|
||||
[subject <String>] [<MessageContent>]
|
||||
(replace <Tag> <String>)*
|
||||
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
|
||||
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
|
||||
(embedimage <FileName> <String>)*
|
||||
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
||||
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
||||
[threadid <String>]
|
||||
gam <UserTypeEntity> sendemail recipient|to <RecipientEntity>
|
||||
[replyto <EmailAddress>]
|
||||
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
||||
[subject <String>]
|
||||
[<MessageContent>]
|
||||
[subject <String>] [<MessageContent>]
|
||||
(replace <Tag> <String>)*
|
||||
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
|
||||
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
|
||||
(embedimage <FileName> <String>)*
|
||||
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
||||
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
||||
[threadid <String>]
|
||||
gam <UserTypeEntity> sendemail from <EmailAddress>
|
||||
[replyto <EmailAddress>]
|
||||
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
||||
[subject <String>]
|
||||
[<MessageContent>]
|
||||
[subject <String>] [<MessageContent>]
|
||||
(replace <Tag> <String>)*
|
||||
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
|
||||
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
|
||||
(embedimage <FileName> <String>)*
|
||||
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
||||
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
||||
[threadid <String>]
|
||||
|
||||
# Shared Drives - Administrator
|
||||
|
||||
@@ -5803,12 +5803,12 @@ gam download storagefile <StorageBucketObjectName>
|
||||
|
||||
<UserClearAttribute> ::=
|
||||
(address clear)|
|
||||
(otheremail clear)|
|
||||
(externalid clear)|
|
||||
(im clear)|
|
||||
(keyword clear)|
|
||||
(location clear)|
|
||||
(organization clear)|
|
||||
(otheremail clear)|
|
||||
(phone clear)|
|
||||
(posix clear)|
|
||||
(relation clear)|
|
||||
@@ -5821,6 +5821,18 @@ gam download storagefile <StorageBucketObjectName>
|
||||
<UserMultiAttribute>|
|
||||
<UserClearAttribute>
|
||||
|
||||
<UserMultiAttributeFilterName> ::=
|
||||
address|addresses|
|
||||
externalid|externalids|
|
||||
im|ims|
|
||||
keyword|keywords|
|
||||
location|locations|
|
||||
orgainzation|organizations|
|
||||
otheremail|otheremails|
|
||||
phone|phones|
|
||||
relation|relations|
|
||||
website|websites
|
||||
|
||||
gam create|add user <EmailAddress> [ignorenullpassword] <UserAttribute>*
|
||||
[verifynotinvitable|alwaysevict]
|
||||
(groups [<GroupRole>] [[delivery] <DeliverySetting>] <GroupEntity>)*
|
||||
@@ -5875,6 +5887,8 @@ gam info user [<UserItem>]
|
||||
[nolicenses|nolicences|licenses|licences]
|
||||
[noschemas|allschemas|(schemas|custom|customschemas <SchemaNameList>)]
|
||||
[userview] <UserFieldName>* [fields <UserFieldNameList>]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
||||
[formatjson]
|
||||
|
||||
@@ -5911,6 +5925,8 @@ gam info users <UserTypeEntity>
|
||||
[nolicenses|nolicences|licenses|licences]
|
||||
[noschemas|allschemas|(schemas|custom|customschemas <SchemaNameList>)]
|
||||
[userview] <UserFieldName>* [fields <UserFieldNameList>]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
||||
[formatjson]
|
||||
|
||||
@@ -5947,6 +5963,8 @@ gam <UserTypeEntity> info users
|
||||
[nolicenses|nolicences|licenses|licences]
|
||||
[noschemas|allschemas|(schemas|custom|customschemas <SchemaNameList>)]
|
||||
[userview] <UserFieldName>* [fields <UserFieldNameList>]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
||||
[formatjson]
|
||||
|
||||
@@ -5966,6 +5984,8 @@ gam print users [todrive <ToDriveAttribute>*]
|
||||
[schemas|custom|customschemas all|<SchemaNameList>]
|
||||
[emailpart|emailparts|username]
|
||||
[userview] [basic|full|allfields|(<UserFieldName>*|fields <UserFieldNameList>)]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[delimiter <Character>] [sortheaders [<Boolean>]] [scalarsfirst [<Boolean>]]
|
||||
[formatjson [quotechar <Character>]] [quoteplusphonenumbers]
|
||||
[issuspended <Boolean>] [isarchived <Boolean>] [aliasmatchpattern <REMatchPattern>]
|
||||
@@ -5983,6 +6003,8 @@ gam print users [todrive <ToDriveAttribute>*] select <UserTypeEntity>
|
||||
[schemas|custom|customschemas all|<SchemaNameList>]
|
||||
[emailpart|emailparts|username]
|
||||
[userview] [basic|full|allfields|(<UserFieldName>*|fields <UserFieldNameList>)]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[delimiter <Character>] [sortheaders [<Boolean>]] [scalarsfirst [<Boolean>]]
|
||||
[formatjson [quotechar <Character>]] [quoteplusphonenumbers]
|
||||
[issuspended <Boolean>] [isarchived <Boolean>] [aliasmatchpattern <REMatchPattern>]
|
||||
@@ -5998,6 +6020,8 @@ gam <UserTypeEntity> print users [todrive <ToDriveAttribute>*]
|
||||
[schemas|custom|customschemas all|<SchemaNameList>]
|
||||
[emailpart|emailparts|username]
|
||||
[userview] [basic|full|allfields|(<UserFieldName>*|fields <UserFieldNameList>)]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[delimiter <Character>] [sortheaders [<Boolean>]] [scalarsfirst [<Boolean>]]
|
||||
[formatjson [quotechar <Character>]] [quoteplusphonenumbers]
|
||||
[issuspended <Boolean>] [isarchived <Boolean>] [aliasmatchpattern <REMatchPattern>]
|
||||
@@ -7498,7 +7522,7 @@ gam <UserTypeEntity> print filecounts [todrive <ToDriveAttribute>*]
|
||||
[filenamematchpattern <REMatchPattern>]
|
||||
<PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
||||
[excludetrashed]
|
||||
[showsize] [showmimetypesize]
|
||||
[showsize] [showsizeunits] [showmimetypesize]
|
||||
[showlastmodification] [pathdelimiter <Character>]
|
||||
(addcsvdata <FieldName> <String>)*
|
||||
[summary none|only|plus] [summaryuser <String>]
|
||||
@@ -7514,7 +7538,7 @@ gam <UserTypeEntity> show filecounts
|
||||
[filenamematchpattern <REMatchPattern>]
|
||||
<PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
||||
[excludetrashed]
|
||||
[showsize] [showmimetypesize]
|
||||
[showsize] [showsizeunits] [showmimetypesize]
|
||||
[showlastmodification] [pathdelimiter <Character>]
|
||||
[summary none|only|plus] [summaryuser <String>]
|
||||
|
||||
@@ -7584,7 +7608,7 @@ gam <UserTypeEntity> print filelist [todrive <ToDriveAttribute>*]
|
||||
[excludetrashed]
|
||||
[maxfiles <Integer>] [nodataheaders <String>]
|
||||
[countsonly [summary none|only|plus] [summaryuser <String>]
|
||||
[showsource] [showsize] [showmimetypesize]]
|
||||
[showsource] [showsize] [showsizeunits] [showmimetypesize]]
|
||||
[countsrowfilter]
|
||||
[filepath|fullpath [folderpathonly [<Boolean>]] [pathdelimiter <Character>] [addpathstojson] [showdepth]] [buildtree]
|
||||
[allfields|<DriveFieldName>*|(fields <DriveFieldNameList>)]
|
||||
|
||||
@@ -1,3 +1,81 @@
|
||||
7.36.02
|
||||
|
||||
Added option `threadid <String>` to `gam [<UserTypeEntity>] sendemail` that causes Gmail to recognize the message
|
||||
in conversation mode in for the user sending the message.
|
||||
|
||||
* See: https://github.com/GAM-team/GAM/wiki/Send-Email#conversation-mode
|
||||
|
||||
7.36.01
|
||||
|
||||
Fixed bug in `gam info|print|show policies` where the `policyQuery/query` field was not displayed.
|
||||
|
||||
Added option `noidmapping` to `gam info|print|show policies` to suppress adding the `policyQuery/groupEmail` and
|
||||
`policyQuery/orgUnitPath` name fields that are mapped from the `policyQuery/group` and `policyQuery/orgInit` id fields.
|
||||
|
||||
7.36.00
|
||||
|
||||
Added options `filtermultiattrtype` and filtermultiattrcustom` to `gam info user` and
|
||||
`gam print users` that support filtering `<UserMultiAttribute>` display based on `type` or `customType`.
|
||||
|
||||
```
|
||||
<UserMultiAttributeFilterName> ::=
|
||||
address|addresses|
|
||||
externalid|externalids|
|
||||
im|ims|
|
||||
keyword|keywords|
|
||||
location|locations|
|
||||
orgainzation|organizations|
|
||||
otheremail|otheremails|
|
||||
phone|phones|
|
||||
relation|relations|
|
||||
website|websites
|
||||
```
|
||||
|
||||
* `filtermultiattrtype <UserMultiAttributeFilterName> <String>` - Display `<UserMultiAttributeFilterName>` if its `type` is `<String>`
|
||||
* `filtermultiattrcustom <UserMultiAttributeFilterName> <String>` - Display `<UserMultiAttributeFilterName>` if its `customType` is `<String>`
|
||||
|
||||
```
|
||||
gam info user user@domain.com quick filtermultiattrtype organizations work filtermultiattrcustom phones private
|
||||
```
|
||||
|
||||
7.35.03
|
||||
|
||||
Updated `gam <UserTypeEntity> print filelist|filecounts` to handle options `showsize` and `showsizeunits` as independent options.
|
||||
* `showsize` - Display a column `Size` with a byte count
|
||||
* `showsizeunits` - Display a column `SizeUnits` with a formatted size with units
|
||||
|
||||
If you select both options, you can sort multiple rows using the `Size` column.
|
||||
|
||||
7.35.02
|
||||
|
||||
Added option `showsizeunits` to `gam gam <UserTypeEntity> print filelist|filecounts` as an alternative to option `showsize`.
|
||||
* `showsize` - 31549200951 - This is a byte count
|
||||
* `showsizeunits` - 31.55 GB - This is as shown in the Admin console
|
||||
|
||||
7.35.01
|
||||
|
||||
The following commands have been updated to not verify the existence of `gam.cfg` credentials files
|
||||
as the WARNING messages about the missing files can be confusing to new users setting up GAM.
|
||||
```
|
||||
gam checkconn
|
||||
gam oauth|oauth2
|
||||
gam version
|
||||
```
|
||||
|
||||
7.35.00
|
||||
|
||||
Windows `gam-7.wx.yz-x86_64.msi` has been replaced with `gam-7.wx.yz-x86_64.exe`.
|
||||
|
||||
Windows `gam-7.wx.yz-arm64.msi` has been replaced with `gam-7.wx.yz-arm64.exe`.
|
||||
|
||||
Updated cacerts.pem to avoid to following error in `gam checkconn`.
|
||||
```
|
||||
Checking raw.githubusercontent.com (185.199.110.133) (2)... ERROR
|
||||
Certificate verification failed. If you are behind a firewall / proxy server that does TLS / SSL inspection you may need to point GAM at your certificate authority file by setting cacerts_pem = /path/to/your/certauth.pem in gam.cfg.
|
||||
```
|
||||
|
||||
If you have customized cacerts.pem, update your version with the `Operating CA: Let's Encrypt` values from the GAM default version.
|
||||
|
||||
7.34.13
|
||||
|
||||
Fixed bug in `gam info policies <CIPolicyNameEntity> ... formatjson` where extraneous line
|
||||
@@ -6483,7 +6561,7 @@ This addresses the following issue:
|
||||
Updated `gam <UserTypeEntity> add|delete|update|print|show datastudiopermissions` to display an appropriate
|
||||
error message, `The caller does not have permission`, when the user doesn't have permission to execute the command.
|
||||
Previously, the following incorrect error message was displayed:
|
||||
`ERROR: Data Studio API not enabled. Please run "gam update project" and "gam user user@domain.com check serviceaccount"`
|
||||
`ERROR: Data Studio API not enabled. Please run "gam update project" and "gam user user@domain.com update serviceaccount"`
|
||||
|
||||
6.26.14
|
||||
|
||||
@@ -6765,7 +6843,7 @@ and display drive labels on files. Please test/experiment and report any issues.
|
||||
To use these commands you must add the 'Drive Labels API' to your project and update your service account authorization.
|
||||
```
|
||||
gam update project
|
||||
gam user user@domain.com check serviceaccount
|
||||
gam user user@domain.com update serviceaccount
|
||||
```
|
||||
Supported editions for this feature: Business Standard and Business Plus; Enterprise; Education Standard and Education Plus; G Suite Business; Essentials.
|
||||
|
||||
@@ -6963,7 +7041,7 @@ ERROR: 403: permissionDenied - Google Forms API has not been used in project XXX
|
||||
```
|
||||
is replaced with
|
||||
```
|
||||
ERROR: Forms API not enabled. Please run "gam update project" and "gam user user@domain.com check serviceaccount"
|
||||
ERROR: Forms API not enabled. Please run "gam update project" and "gam user user@domain.com update serviceaccount"
|
||||
```
|
||||
|
||||
6.23.00
|
||||
@@ -9321,7 +9399,7 @@ To use this feature you must add the `People API` to your project and authorize
|
||||
* `People API - Other Contacts - read only`: https://www.googleapis.com/auth/contacts.other.readonly
|
||||
```
|
||||
gam update project
|
||||
gam user user@domain.com check serviceaccount
|
||||
gam user user@domain.com update serviceaccount
|
||||
```
|
||||
|
||||
Added commands to display user's contact groups using the People API.
|
||||
@@ -9362,7 +9440,7 @@ To use these features you must add the `People API` to your project and authoriz
|
||||
```
|
||||
gam update project
|
||||
gam oauth create
|
||||
gam user user@domain.com check serviceaccount
|
||||
gam user user@domain.com update serviceaccount
|
||||
```
|
||||
|
||||
Following Jay's lead, added new license SKU `Cloud Search`.
|
||||
@@ -9401,7 +9479,7 @@ Added commands to display Data Studio assets and display/manage Data Studio perm
|
||||
To use these commands you must add the `Data Studio API` to your project and update your service account authorization.
|
||||
```
|
||||
gam update project
|
||||
gam user user@domain.com check serviceaccount
|
||||
gam user user@domain.com update serviceaccount
|
||||
```
|
||||
This is a first release from me, experiment and use with caution.
|
||||
|
||||
@@ -10444,7 +10522,7 @@ Added commands to support the new Device Management API.
|
||||
|
||||
To use these commands you must update your service account authorization.
|
||||
```
|
||||
gam user user@domain.com check serviceaccount
|
||||
gam user user@domain.com update serviceaccount
|
||||
```
|
||||
|
||||
In the following places a Google Admin email address is required; by default the admin email address in `oauth2.txt` is used.
|
||||
|
||||
116
src/gam.iss
Normal file
116
src/gam.iss
Normal file
@@ -0,0 +1,116 @@
|
||||
; --- 1. PREPROCESSOR DEFINITIONS ---
|
||||
#define AppVersion GetEnv("GAMVERSION")
|
||||
#if AppVersion == ""
|
||||
#define AppVersion "7.0.0"
|
||||
#endif
|
||||
|
||||
; Pull architecture directly from GitHub Actions environment variable
|
||||
#define RunnerArch GetEnv("RUNNER_ARCH")
|
||||
|
||||
[Setup]
|
||||
; --- 2. CORE APPLICATION INFO ---
|
||||
AppId={{D86B52B2-EFE9-4F9D-8BA3-9D84B9B2D319}
|
||||
AppName=GAM7
|
||||
AppVersion={#AppVersion}
|
||||
AppPublisher=GAM Team - google-apps-manager@googlegroups.com
|
||||
DefaultDirName={sd}\GAM7
|
||||
LicenseFile=dist\gam\gam7\LICENSE
|
||||
PrivilegesRequired=admin
|
||||
ChangesEnvironment=yes
|
||||
|
||||
; Tell Inno Setup to use a custom signtool defined via the command line
|
||||
SignTool=gamsigntool
|
||||
|
||||
; --- 3. COMPRESSION & OPTIMIZATION ---
|
||||
Compression=lzma2/ultra64
|
||||
SolidCompression=yes
|
||||
|
||||
; --- 4. DYNAMIC ARCHITECTURE CONFIGURATION ---
|
||||
; GitHub Actions RUNNER_ARCH is typically uppercase "ARM64" or "X64"
|
||||
#if RunnerArch == "ARM64" || RunnerArch == "arm64"
|
||||
ArchitecturesAllowed=arm64
|
||||
ArchitecturesInstallIn64BitMode=arm64
|
||||
OutputBaseFilename=gam-{#AppVersion}-windows-arm64
|
||||
#else
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
OutputBaseFilename=gam-{#AppVersion}-windows-x86_64
|
||||
#endif
|
||||
|
||||
[Messages]
|
||||
; Custom error if an admin tries to run the ARM64 installer on an Intel machine
|
||||
#if RunnerArch == "ARM64" || RunnerArch == "arm64"
|
||||
WindowsVersionNotSupported=You downloaded the ARM64 version of GAM, but this computer has an Intel or AMD processor.%n%nPlease go back to the release page and download the x86_64 installer instead.
|
||||
#endif
|
||||
|
||||
[Files]
|
||||
; --- 5. DYNAMIC FILE INCLUSION ---
|
||||
Source: "dist\gam\gam7\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Registry]
|
||||
; --- 6. PATH ENVIRONMENT VARIABLE ---
|
||||
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
|
||||
ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \
|
||||
Check: NeedsAddPath(ExpandConstant('{app}'))
|
||||
|
||||
[Code]
|
||||
const
|
||||
ERROR_SUCCESS = 0;
|
||||
|
||||
function MsiEnumRelatedProducts(lpUpgradeCode: string; dwReserved: Integer; iProductIndex: Integer; lpProductBuf: string): Integer;
|
||||
external 'MsiEnumRelatedProductsW@msi.dll stdcall';
|
||||
|
||||
function UninstallWixMSI(): Boolean;
|
||||
var
|
||||
UpgradeCode: string;
|
||||
ProductCode: string;
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
UpgradeCode := '{D86B52B2-EFE9-4F9D-8BA3-9D84B9B2D319}';
|
||||
ProductCode := StringOfChar(' ', 39);
|
||||
|
||||
ResultCode := MsiEnumRelatedProducts(UpgradeCode, 0, 0, ProductCode);
|
||||
|
||||
if ResultCode = ERROR_SUCCESS then
|
||||
begin
|
||||
ProductCode := Trim(ProductCode);
|
||||
Exec('msiexec.exe', '/x ' + ProductCode + ' /qn /norestart', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function InitializeSetup(): Boolean;
|
||||
begin
|
||||
// --- Architecture Warning for Emulation ---
|
||||
#if RunnerArch != "ARM64" && RunnerArch != "arm64"
|
||||
if IsArm64() then
|
||||
begin
|
||||
if MsgBox('Notice: You are installing the Intel (x86_64) build of GAM on an ARM processor.' + #13#10#13#10 +
|
||||
'While this will work via Windows emulation, it will perform worse than the native ARM64 version.' + #13#10#13#10 +
|
||||
'Do you want to continue with the installation anyway?',
|
||||
mbConfirmation, MB_YESNO) = idNo then
|
||||
begin
|
||||
Result := False;
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
#endif
|
||||
|
||||
UninstallWixMSI();
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function NeedsAddPath(Param: string): boolean;
|
||||
var
|
||||
OrigPath: string;
|
||||
begin
|
||||
if not RegQueryStringValue(HKEY_LOCAL_MACHINE,
|
||||
'SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
|
||||
'Path', OrigPath)
|
||||
then begin
|
||||
Result := True;
|
||||
exit;
|
||||
end;
|
||||
Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0;
|
||||
end;
|
||||
@@ -8,7 +8,7 @@
|
||||
Manufacturer="GAM Team - google-apps-manager@googlegroups.com"
|
||||
UpgradeCode="D86B52B2-EFE9-4F9D-8BA3-9D84B9B2D319">
|
||||
<Package
|
||||
InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
|
||||
InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />
|
||||
|
||||
<MajorUpgrade
|
||||
DowngradeErrorMessage=
|
||||
|
||||
@@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki
|
||||
"""
|
||||
|
||||
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
|
||||
__version__ = '7.34.13'
|
||||
__version__ = '7.36.02'
|
||||
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
@@ -204,11 +204,13 @@ ERROR_PREFIX = ERROR+': '
|
||||
WARNING = 'WARNING'
|
||||
WARNING_PREFIX = WARNING+': '
|
||||
ONE_KILO_10_BYTES = 1000
|
||||
ONE_MEGA_10_BYTES = 1000000
|
||||
ONE_GIGA_10_BYTES = 1000000000
|
||||
ONE_MEGA_10_BYTES = ONE_KILO_10_BYTES*ONE_KILO_10_BYTES
|
||||
ONE_GIGA_10_BYTES = ONE_KILO_10_BYTES*ONE_MEGA_10_BYTES
|
||||
ONE_TERA_10_BYTES = ONE_KILO_10_BYTES*ONE_GIGA_10_BYTES
|
||||
ONE_KILO_BYTES = 1024
|
||||
ONE_MEGA_BYTES = 1048576
|
||||
ONE_GIGA_BYTES = 1073741824
|
||||
ONE_MEGA_BYTES = ONE_KILO_BYTES*ONE_KILO_BYTES
|
||||
ONE_GIGA_BYTES = ONE_KILO_BYTES*ONE_MEGA_BYTES
|
||||
ONE_TERA_BYTES = ONE_KILO_BYTES*ONE_GIGA_BYTES
|
||||
DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
SECONDS_PER_MINUTE = 60
|
||||
SECONDS_PER_HOUR = 3600
|
||||
@@ -2382,16 +2384,18 @@ def splitEmailAddress(emailAddress):
|
||||
return (emailAddress.lower(), GC.Values[GC.DOMAIN])
|
||||
return (emailAddress[:atLoc].lower(), emailAddress[atLoc+1:].lower())
|
||||
|
||||
def formatFileSize(fileSize):
|
||||
if fileSize == 0:
|
||||
return '0kb'
|
||||
if fileSize < ONE_KILO_10_BYTES:
|
||||
return '1kb'
|
||||
if fileSize < ONE_MEGA_10_BYTES:
|
||||
return f'{fileSize//ONE_KILO_10_BYTES}kb'
|
||||
if fileSize < ONE_GIGA_10_BYTES:
|
||||
return f'{fileSize//ONE_MEGA_10_BYTES}mb'
|
||||
return f'{fileSize//ONE_GIGA_10_BYTES}gb'
|
||||
def formatFileSize(size):
|
||||
if size == 0:
|
||||
return '0 KB'
|
||||
if size < ONE_KILO_10_BYTES:
|
||||
return '1 KB'
|
||||
if size < ONE_MEGA_10_BYTES:
|
||||
return f'{size/ONE_KILO_10_BYTES:.2f} KB'
|
||||
if size < ONE_GIGA_10_BYTES:
|
||||
return f'{size/ONE_MEGA_10_BYTES:.2f} MB'
|
||||
if size < ONE_TERA_10_BYTES:
|
||||
return f'{size/ONE_GIGA_10_BYTES:.2f} GB'
|
||||
return f'{size/ONE_TERA_10_BYTES:.2f} TB'
|
||||
|
||||
def formatLocalTime(dateTimeStr):
|
||||
if dateTimeStr in {NEVER_TIME, NEVER_TIME_NOMS}:
|
||||
@@ -4349,7 +4353,8 @@ def SetGlobalVariables():
|
||||
# warn if the json files are missing and return True
|
||||
if (Cmd.Location() == 1) or (Cmd.ArgumentsRemaining()):
|
||||
_chkCfgDirectories(sectionName)
|
||||
_chkCfgFiles(sectionName)
|
||||
if not Cmd.PeekArgumentPresent(['checkconn', 'checkconnection', 'comment', 'oauth', 'oauth2', 'version']):
|
||||
_chkCfgFiles(sectionName)
|
||||
if status['errors']:
|
||||
sys.exit(CONFIG_ERROR_RC)
|
||||
if GC.Values[GC.NO_CACHE]:
|
||||
@@ -7342,7 +7347,7 @@ def _addEmbeddedImagesToMessage(message, embeddedImages):
|
||||
# Send an email
|
||||
def send_email(msgSubject, msgBody, msgTo, i=0, count=0, clientAccess=False, msgFrom=None, msgReplyTo=None,
|
||||
html=False, charset=UTF8, attachments=None, embeddedImages=None,
|
||||
msgHeaders=None, ccRecipients=None, bccRecipients=None, mailBox=None):
|
||||
msgHeaders=None, ccRecipients=None, bccRecipients=None, mailBox=None, threadId=None):
|
||||
def checkResult(entityType, recipients):
|
||||
if not recipients:
|
||||
return
|
||||
@@ -7409,11 +7414,14 @@ def send_email(msgSubject, msgBody, msgTo, i=0, count=0, clientAccess=False, msg
|
||||
userId = mailBoxAddr
|
||||
gmail = buildGAPIObject(API.GMAIL)
|
||||
message['To'] = msgTo if msgTo else userId
|
||||
body = {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}
|
||||
if threadId is not None:
|
||||
body['threadId'] = threadId
|
||||
try:
|
||||
result = callGAPI(gmail.users().messages(), 'send',
|
||||
throwReasons=[GAPI.SERVICE_NOT_AVAILABLE, GAPI.AUTH_ERROR, GAPI.DOMAIN_POLICY,
|
||||
GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
||||
userId=userId, body={'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}, fields='id')
|
||||
userId=userId, body=body, fields='id')
|
||||
entityActionPerformedMessage([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], f"{result['id']}", i, count)
|
||||
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy,
|
||||
GAPI.invalid, GAPI.invalidArgument, GAPI.forbidden, GAPI.permissionDenied) as e:
|
||||
@@ -9499,6 +9507,33 @@ def getOSPlatform():
|
||||
pltfrm = platform.platform()
|
||||
return f'{myos} {pltfrm}'
|
||||
|
||||
def inspect_untrusted_cert(url):
|
||||
"""Bypasses validation momentarily to extract the untrusted Issuer."""
|
||||
parsed = urlparse(url if '://' in url else f'https://{url}')
|
||||
host = parsed.hostname
|
||||
port = parsed.port or 443
|
||||
# Create an unverified context purely for diagnostic extraction
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=5) as sock:
|
||||
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
der_cert = ssock.getpeercert(binary_form=True)
|
||||
cert = x509.load_der_x509_certificate(der_cert, default_backend())
|
||||
issuer = cert.issuer.rfc4514_string()
|
||||
subject = cert.subject.rfc4514_string()
|
||||
try:
|
||||
san_ext = cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
|
||||
# Loop through the list of SANs (DNS names, IP addresses, etc.)
|
||||
sans = [str(name.value) for name in san_ext.value]
|
||||
san_str = ", ".join(sans)
|
||||
except x509.ExtensionNotFound:
|
||||
san_str = "None"
|
||||
return f"Untrusted Issuer: {issuer}\n Server Subject: {subject}\n SANs: {san_str}"
|
||||
except Exception as e:
|
||||
return f"Failed to retrieve diagnostic certificate: {e}"
|
||||
|
||||
# gam checkconnection
|
||||
def doCheckConnection():
|
||||
|
||||
@@ -9534,6 +9569,10 @@ def doCheckConnection():
|
||||
writeStdout(f'{not_okay}\n Connection reset by peer. {gen_firewall}\n')
|
||||
except httplib2.error.ServerNotFoundError:
|
||||
writeStdout(f'{not_okay}\n Failed to find server. Your DNS is probably misconfigured.\n')
|
||||
except ssl.SSLCertVerificationError as e:
|
||||
diag_info = inspect_untrusted_cert(host)
|
||||
# e.verify_message contains the specific OpenSSL error string
|
||||
writeStdout(f'{not_okay}\n Certificate verification failed: {e.verify_message}\n Diagnostic Info:\n {diag_info}\nIf you are behind a firewall / proxy server that does TLS / SSL inspection you may need to point GAM at your certificate authority file by setting cacerts_pem = /path/to/your/certauth.pem in gam.cfg.\n')
|
||||
except ssl.SSLError as e:
|
||||
if e.reason == 'SSLV3_ALERT_HANDSHAKE_FAILURE':
|
||||
writeStdout(f'{not_okay}\n GAM expects to connect with TLS 1.3 or newer and that failed. If your firewall / proxy server is not compatible with TLS 1.3 then you can tell GAM to allow TLS 1.2 by setting tls_min_version = TLSv1.2 in gam.cfg.\n')
|
||||
@@ -15354,34 +15393,34 @@ def getRecipients():
|
||||
|
||||
# gam sendemail [recipient|to] <RecipientEntity> [from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
|
||||
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
||||
# [subject <String>]
|
||||
# [<MessageContent>]
|
||||
# [subject <String>] [<MessageContent>]
|
||||
# (replace <Tag> <String>)*
|
||||
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
|
||||
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
|
||||
# (embedimage <FileName> <String>)*
|
||||
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
||||
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
||||
# [threadid <String>]
|
||||
# gam <UserTypeEntity> sendemail recipient|to <RecipientEntity> [replyto <EmailAddress>]
|
||||
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
||||
# [subject <String>]
|
||||
# [<MessageContent>]
|
||||
# [subject <String>] [<MessageContent>]
|
||||
# (replace <Tag> <String>)*
|
||||
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
|
||||
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
|
||||
# (embedimage <FileName> <String>)*
|
||||
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
||||
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
||||
# [threadid <String>]
|
||||
# gam <UserTypeEntity> sendemail from <EmailAddress> [replyto <EmailAddress>]
|
||||
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
||||
# [subject <String>]
|
||||
# [<MessageContent>]
|
||||
# [subject <String>] [<MessageContent>]
|
||||
# (replace <Tag> <String>)*
|
||||
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
|
||||
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
|
||||
# (embedimage <FileName> <String>)*
|
||||
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
||||
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
||||
# [threadid <String>]
|
||||
def doSendEmail(users=None):
|
||||
body = {}
|
||||
notify = {'subject': '', 'message': '', 'html': False, 'charset': UTF8, 'password': ''}
|
||||
@@ -15406,6 +15445,7 @@ def doSendEmail(users=None):
|
||||
bccRecipients = []
|
||||
mailBox = None
|
||||
msgReplyTo = None
|
||||
threadId = None
|
||||
singleMessage = False
|
||||
tagReplacements = _initTagReplacements()
|
||||
attachments = []
|
||||
@@ -15456,6 +15496,8 @@ def doSendEmail(users=None):
|
||||
elif myarg == 'header':
|
||||
header = getString(Cmd.OB_STRING, minLen=1)
|
||||
msgHeaders[SMTP_HEADERS_MAP.get(header.lower(), header)] = getString(Cmd.OB_STRING)
|
||||
elif myarg == 'threadid':
|
||||
threadId = getString(Cmd.OB_STRING)
|
||||
else:
|
||||
unknownArgumentExit()
|
||||
notify['message'] = notify['message'].replace('\r', '').replace('\\n', '\n')
|
||||
@@ -15483,7 +15525,7 @@ def doSendEmail(users=None):
|
||||
msgFrom=msgFrom, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'],
|
||||
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders,
|
||||
ccRecipients=','.join(ccRecipients), bccRecipients=','.join(bccRecipients),
|
||||
mailBox=mailBox)
|
||||
mailBox=mailBox, threadId=threadId)
|
||||
else:
|
||||
entityPerformActionModifierNumItems([Ent.USER, msgFrom], Act.MODIFIER_TO, jcount, Ent.RECIPIENT, i, count)
|
||||
Ind.Increment()
|
||||
@@ -15492,7 +15534,7 @@ def doSendEmail(users=None):
|
||||
j += 1
|
||||
send_email(notify['subject'], notify['message'], recipient, j, jcount,
|
||||
msgFrom=msgFrom, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'],
|
||||
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders, mailBox=mailBox)
|
||||
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders, mailBox=mailBox, threadId=threadId)
|
||||
Ind.Decrement()
|
||||
|
||||
ADDRESS_FIELDS_PRINT_ORDER = ['contactName', 'organizationName', 'addressLine1', 'addressLine2', 'addressLine3', 'locality', 'region', 'postalCode', 'countryCode']
|
||||
@@ -37707,9 +37749,7 @@ def _filterPolicies(ci, pageMessage, ifilter):
|
||||
policies = callGAPIpages(ci.policies(), 'list', 'policies',
|
||||
pageMessage=pageMessage,
|
||||
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
||||
filter=ifilter,
|
||||
fields='nextPageToken,policies(name,policyQuery(group,orgUnit,sortOrder),type,setting)',
|
||||
pageSize=100)
|
||||
filter=ifilter, pageSize=100)
|
||||
# Google returns unordered results, sort them by setting type
|
||||
return sorted(policies, key=lambda p: p.get('setting', {}).get('type', ''))
|
||||
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
||||
@@ -37741,12 +37781,19 @@ def _getPolicyAppNameFromId(httpObj, app):
|
||||
if a:
|
||||
app['applicationName'] = a.group(1)
|
||||
|
||||
def _cleanPolicy(policy, add_warnings, no_appnames,
|
||||
def _cleanPolicy(policy, add_warnings, no_appnames, no_idmapping,
|
||||
groupEmailPattern, orgUnitPathPattern,
|
||||
cd, groups_ci):
|
||||
# convert any wordlists into spaced strings to reduce output complexity
|
||||
if policy['setting']['type'] == 'settings/detector.word_list':
|
||||
policy['setting']['value']['wordList'] = ' '.join(policy['setting']['value']['wordList']['words'])
|
||||
wordList = ''
|
||||
for word in policy['setting']['value']['wordList']['words']:
|
||||
wordList += "'"
|
||||
wordList += word.replace("'", r"\'")
|
||||
wordList += "',"
|
||||
if wordList:
|
||||
wordList = wordList[:-1]
|
||||
policy['setting']['value']['wordList'] = wordList
|
||||
# get application name for application id
|
||||
if policy['setting']['type'] == 'settings/workspace_marketplace.apps_allowlist' and not no_appnames:
|
||||
httpObj = getHttpObj(timeout=10)
|
||||
@@ -37756,19 +37803,19 @@ def _cleanPolicy(policy, add_warnings, no_appnames,
|
||||
if add_warnings and policy['setting']['type'] in CIPOLICY_ADDITIONAL_WARNINGS:
|
||||
policy['warning'] = CIPOLICY_ADDITIONAL_WARNINGS[policy['setting']['type']]
|
||||
if groupId := policy['policyQuery'].get('group'):
|
||||
_, _, policy['policyQuery']['groupEmail'] = convertGroupCloudIDToEmail(groups_ci, groupId)
|
||||
# all groups are in the root OU so the orgUnit attribute is useless
|
||||
policy['policyQuery'].pop('orgUnit', None)
|
||||
if groupEmailPattern is not None:
|
||||
return groupEmailPattern.match(policy['policyQuery']['groupEmail'])
|
||||
if orgUnitPathPattern is not None:
|
||||
return False
|
||||
if (not no_idmapping) or (groupEmailPattern is not None):
|
||||
_, _, groupEmail = convertGroupCloudIDToEmail(groups_ci, groupId)
|
||||
if not no_idmapping:
|
||||
policy['policyQuery']['groupEmail'] = groupEmail
|
||||
if groupEmailPattern is not None:
|
||||
return groupEmailPattern.match(groupEmail)
|
||||
elif orgId := policy['policyQuery'].get('orgUnit'):
|
||||
policy['policyQuery']['orgUnitPath'] = convertOrgUnitIDtoPath(cd, orgId)
|
||||
if orgUnitPathPattern is not None:
|
||||
return orgUnitPathPattern.match(policy['policyQuery']['orgUnitPath'])
|
||||
if groupEmailPattern is not None:
|
||||
return False
|
||||
if (not no_idmapping) or (orgUnitPathPattern is not None):
|
||||
orgUnitPath = convertOrgUnitIDtoPath(cd, orgId)
|
||||
if not no_idmapping:
|
||||
policy['policyQuery']['orgUnitPath'] = orgUnitPath
|
||||
if orgUnitPathPattern is not None:
|
||||
return orgUnitPathPattern.match(orgUnitPath)
|
||||
return True
|
||||
|
||||
def _showPolicy(policy, FJQC, i=0, count=0):
|
||||
@@ -37783,9 +37830,8 @@ def _showPolicy(policy, FJQC, i=0, count=0):
|
||||
printBlankLine()
|
||||
Ind.Decrement()
|
||||
|
||||
def _showPolicies(policies, FJQC, add_warnings, no_appnames,
|
||||
groupEmailPattern, orgUnitPathPattern,
|
||||
cd, groups_ci):
|
||||
def _showPolicies(policies, FJQC, add_warnings, no_appnames, no_idmapping,
|
||||
groupEmailPattern, orgUnitPathPattern, cd, groups_ci):
|
||||
count = len(policies)
|
||||
if FJQC is None or not FJQC.formatJSON:
|
||||
if groupEmailPattern is None and orgUnitPathPattern is None:
|
||||
@@ -37796,9 +37842,8 @@ def _showPolicies(policies, FJQC, add_warnings, no_appnames,
|
||||
i = 0
|
||||
for policy in policies:
|
||||
i += 1
|
||||
if _cleanPolicy(policy, add_warnings, no_appnames,
|
||||
groupEmailPattern, orgUnitPathPattern,
|
||||
cd, groups_ci):
|
||||
if _cleanPolicy(policy, add_warnings, no_appnames, no_idmapping,
|
||||
groupEmailPattern, orgUnitPathPattern, cd, groups_ci):
|
||||
_showPolicy(policy, FJQC, i, count)
|
||||
Ind.Decrement()
|
||||
|
||||
@@ -37835,49 +37880,55 @@ def doCreateUpdateCIPolicy():
|
||||
updateCmd = Act.Get() == Act.UPDATE
|
||||
groupEmail = orgUnit = None
|
||||
checkArgumentPresent('json', True)
|
||||
jsonData = getJSON(['customer', 'type'])
|
||||
policy = getJSON(['customer', 'type'])
|
||||
if updateCmd:
|
||||
pname = jsonData.pop('name', None)
|
||||
pname = policy.pop('name', None)
|
||||
else:
|
||||
jsonData.pop('name', None)
|
||||
policy.pop('name', None)
|
||||
pname = 'New Policy'
|
||||
if 'policyQuery' in jsonData:
|
||||
jsonData['policyQuery'].pop('orgUnitPath', None)
|
||||
jsonData['policyQuery'].pop('groupEmail', None)
|
||||
jsonData['policyQuery'].pop('sortOrder', None)
|
||||
if 'setting' in jsonData:
|
||||
if 'value' in jsonData['setting']:
|
||||
jsonData['setting']['value'].pop('createTime', None)
|
||||
jsonData['setting']['value'].pop('updateTime', None)
|
||||
if 'policyQuery' in policy:
|
||||
policy['policyQuery'].pop('orgUnitPath', None)
|
||||
policy['policyQuery'].pop('groupEmail', None)
|
||||
policy['policyQuery'].pop('sortOrder', None)
|
||||
if 'setting' in policy:
|
||||
if 'value' in policy['setting']:
|
||||
policy['setting']['value'].pop('createTime', None)
|
||||
policy['setting']['value'].pop('deleteTime', None)
|
||||
policy['setting']['value'].pop('updateTime', None)
|
||||
if policy['setting']['type'] == 'settings/detector.word_list':
|
||||
if isinstance(policy['setting']['value']['wordList'], str):
|
||||
wordList = policy['setting']['value'].pop('wordList')
|
||||
policy['setting']['value']['wordList']['words'] = shlexSplitList(wordList, dataDelimiter=',')
|
||||
while Cmd.ArgumentsRemaining():
|
||||
myarg = getArgument()
|
||||
if myarg in {'ou', 'org', 'orgunit'}:
|
||||
orgUnit, targetResource = _getCIPolicyOrgUnitTarget(cd, myarg, groupEmail)
|
||||
jsonData.setdefault('policyQuery', {})
|
||||
jsonData['policyQuery'].pop('group', None)
|
||||
jsonData['policyQuery']['orgUnit'] = targetResource
|
||||
policy.setdefault('policyQuery', {})
|
||||
policy['policyQuery'].pop('group', None)
|
||||
policy['policyQuery']['orgUnit'] = f"orgUnits/{targetResource}"
|
||||
policy['policyQuery']['query'] = f"entity.org_units.exists(org_unit, org_unit.org_unit_id == orgUnitId('{targetResource}'))"
|
||||
elif myarg == 'group':
|
||||
groupEmail, targetResource = _getCIPolicyGroupTarget(cd, myarg, orgUnit)
|
||||
jsonData.setdefault('policyQuery', {})
|
||||
jsonData['policyQuery'].pop('orgUnit', None)
|
||||
jsonData['policyQuery']['group'] = targetResource
|
||||
policy.setdefault('policyQuery', {})
|
||||
policy['policyQuery'].pop('orgUnit', None)
|
||||
policy['policyQuery']['group'] = f"groups/{targetResource}"
|
||||
policy['policyQuery']['query'] = f"entity.groups.exists(group, group.group_id == groupId('{targetResource}'))"
|
||||
else:
|
||||
unknownArgumentExit()
|
||||
jsonData['customer'] = _getCustomersCustomerIdWithC()
|
||||
policy['customer'] = _getCustomersCustomerIdWithC()
|
||||
try:
|
||||
if updateCmd:
|
||||
result = callGAPI(ci.policies(), 'patch',
|
||||
bailOnInternalError=True,
|
||||
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.UNIMPLEMENTED_ERROR,
|
||||
GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
||||
|
||||
name=pname, body=jsonData)
|
||||
name=pname, body=policy)
|
||||
else:
|
||||
result = callGAPI(ci.policies(), 'create',
|
||||
bailOnInternalError=True,
|
||||
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.UNIMPLEMENTED_ERROR,
|
||||
GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
||||
body=jsonData)
|
||||
body=policy)
|
||||
if result['done']:
|
||||
if 'error' not in result:
|
||||
if not updateCmd:
|
||||
@@ -37943,7 +37994,7 @@ def doDeleteCIPolicies():
|
||||
Ind.Decrement()
|
||||
|
||||
# gam info policies <CIPolicyNameEntity>
|
||||
# [nowarnings] [noappnames]
|
||||
# [nowarnings] [noappnames] [noidmappiong]
|
||||
# [formatjson]
|
||||
def doInfoCIPolicies():
|
||||
_checkPoliciesWithDASA()
|
||||
@@ -37953,13 +38004,15 @@ def doInfoCIPolicies():
|
||||
entityList = getEntityList(Cmd.OB_CIPOLICY_NAME_ENTITY)
|
||||
FJQC = FormatJSONQuoteChar()
|
||||
add_warnings = True
|
||||
no_appnames = False
|
||||
no_appnames = no_idmapping = False
|
||||
while Cmd.ArgumentsRemaining():
|
||||
myarg = getArgument()
|
||||
if myarg == 'nowarnings':
|
||||
add_warnings = False
|
||||
elif myarg == 'noappnames':
|
||||
no_appnames=True
|
||||
no_appnames = True
|
||||
elif myarg == 'noidmapping':
|
||||
no_idmapping = True
|
||||
else:
|
||||
FJQC.GetFormatJSON(myarg)
|
||||
i = 0
|
||||
@@ -37972,7 +38025,7 @@ def doInfoCIPolicies():
|
||||
bailOnInternalError=True,
|
||||
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT,
|
||||
GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
||||
name=pname, fields='name,policyQuery(group,orgUnit,sortOrder),type,setting')]
|
||||
name=pname)]
|
||||
except (GAPI.invalid, GAPI.invalidArgument, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e:
|
||||
entityActionFailedWarning([Ent.POLICY, pname], str(e), i, count)
|
||||
continue
|
||||
@@ -37982,15 +38035,15 @@ def doInfoCIPolicies():
|
||||
ifilter = f"setting.type.matches('{pname}')"
|
||||
printGettingAllAccountEntities(Ent.POLICY, ifilter)
|
||||
policies = _filterPolicies(ci, getPageMessage(), ifilter)
|
||||
_showPolicies(policies, FJQC, add_warnings, no_appnames,
|
||||
_showPolicies(policies, FJQC, add_warnings, no_appnames, no_idmapping,
|
||||
None, None, cd, groups_ci)
|
||||
|
||||
# gam print policies [todrive <ToDriveAttribute>*]
|
||||
# [filter <String>] [nowarnings] [noappnames]
|
||||
# [filter <String>] [nowarnings] [noappnames] [noidmappiong]
|
||||
# [group <REMatchPattern>] [ou|org|orgunit <REMatchPattern>]
|
||||
# [formatjson [quotechar <Character>]]
|
||||
# gam show policies
|
||||
# [filter <String>] [nowarnings] [noappnames]
|
||||
# [filter <String>] [nowarnings] [noappnames] [noidmappiong]
|
||||
# [group <REMatchPattern>] [ou|org|orgunit <REMatchPattern>]
|
||||
# [formatjson]
|
||||
def doPrintShowCIPolicies():
|
||||
@@ -38012,7 +38065,7 @@ def doPrintShowCIPolicies():
|
||||
FJQC = FormatJSONQuoteChar(csvPF)
|
||||
ifilter = None
|
||||
add_warnings = True
|
||||
no_appnames = False
|
||||
no_appnames = no_idmapping = False
|
||||
groupEmailPattern = orgUnitPathPattern = None
|
||||
while Cmd.ArgumentsRemaining():
|
||||
myarg = getArgument()
|
||||
@@ -38024,6 +38077,8 @@ def doPrintShowCIPolicies():
|
||||
add_warnings = False
|
||||
elif myarg == 'noappnames':
|
||||
no_appnames = True
|
||||
elif myarg == 'noidmapping':
|
||||
no_idmapping = True
|
||||
elif myarg == 'group':
|
||||
groupEmailPattern = getREPattern(re.IGNORECASE)
|
||||
elif myarg in {'ou', 'org', 'orgunit'}:
|
||||
@@ -38033,14 +38088,12 @@ def doPrintShowCIPolicies():
|
||||
printGettingAllAccountEntities(Ent.POLICY, ifilter)
|
||||
policies = _filterPolicies(ci, getPageMessage(), ifilter)
|
||||
if not csvPF:
|
||||
_showPolicies(policies, FJQC, add_warnings, no_appnames,
|
||||
groupEmailPattern, orgUnitPathPattern,
|
||||
cd, groups_ci)
|
||||
_showPolicies(policies, FJQC, add_warnings, no_appnames, no_idmapping,
|
||||
groupEmailPattern, orgUnitPathPattern, cd, groups_ci)
|
||||
else:
|
||||
for policy in policies:
|
||||
if _cleanPolicy(policy, add_warnings, no_appnames,
|
||||
groupEmailPattern, orgUnitPathPattern,
|
||||
cd, groups_ci):
|
||||
if _cleanPolicy(policy, add_warnings, no_appnames, no_idmapping,
|
||||
groupEmailPattern, orgUnitPathPattern, cd, groups_ci):
|
||||
_printPolicy(policy)
|
||||
if csvPF:
|
||||
csvPF.writeCSVfile('Policies')
|
||||
@@ -39660,7 +39713,7 @@ def doCreateFeature():
|
||||
callGAPI(cd.resources().features(), 'insert',
|
||||
throwReasons=[GAPI.DUPLICATE, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
||||
customer=GC.Values[GC.CUSTOMER_ID], body=body)
|
||||
entityActionPerformed([Ent.BUILDING, body['name']])
|
||||
entityActionPerformed([Ent.FEATURE, body['name']])
|
||||
except GAPI.duplicate:
|
||||
entityDuplicateWarning([Ent.FEATURE, body['name']])
|
||||
except GAPI.invalidInput as e:
|
||||
@@ -46690,10 +46743,58 @@ USER_FIELDS_CHOICE_MAP = {
|
||||
'websites': 'websites',
|
||||
}
|
||||
|
||||
USER_MULTI_ATTR_FILTER_CHOICE_MAP = {
|
||||
'address': 'addresses',
|
||||
'addresses': 'addresses',
|
||||
'email': 'emails',
|
||||
'emails': 'emails',
|
||||
'externalid': 'externalIds',
|
||||
'externalids': 'externalIds',
|
||||
'im': 'ims',
|
||||
'ims': 'ims',
|
||||
'keyword': 'keywords',
|
||||
'keywords': 'keywords',
|
||||
'location': 'locations',
|
||||
'locations': 'locations',
|
||||
'organization': 'organizations',
|
||||
'organizations': 'organizations',
|
||||
'organisation': 'organizations',
|
||||
'organisations': 'organizations',
|
||||
'otheremail': 'emails',
|
||||
'otheremails': 'emails',
|
||||
'phone': 'phones',
|
||||
'phones': 'phones',
|
||||
'relation': 'relations',
|
||||
'relations': 'relations',
|
||||
'website': 'websites',
|
||||
'websites': 'websites',
|
||||
}
|
||||
|
||||
INFO_USER_OPTIONS = {'noaliases', 'nobuildingnames', 'nogroups', 'nolicenses', 'nolicences', 'noschemas', 'schemas', 'userview'}
|
||||
USER_SKIP_OBJECTS = {'thumbnailPhotoEtag'}
|
||||
USER_TIME_OBJECTS = {'creationTime', 'deletionTime', 'lastLoginTime'}
|
||||
|
||||
def _getUserMultiAttributeFilters(myarg, userMultiAttributeFilters):
|
||||
up = getChoice(USER_MULTI_ATTR_FILTER_CHOICE_MAP, mapChoice=True)
|
||||
filterValue = getString(Cmd.OB_STRING)
|
||||
userMultiAttributeFilters.setdefault(up, [])
|
||||
if myarg == 'filtermultiattrtype':
|
||||
userMultiAttributeFilters[up].append({'type': filterValue})
|
||||
else: #elif myarg == 'filtermultiattrcustom':
|
||||
userMultiAttributeFilters[up].append({'customType': filterValue})
|
||||
|
||||
def _filterUserMultiAttributes(user, userMultiAttributeFilters):
|
||||
for up, upTypes in userMultiAttributeFilters.items():
|
||||
if up in user:
|
||||
filterAttrList = []
|
||||
for userAttr in user.pop(up):
|
||||
for upType in upTypes:
|
||||
if ((userAttr.get('type', None) == upType.get('type', '')) or
|
||||
(userAttr.get('customType', None) == upType.get('customType', ''))):
|
||||
filterAttrList.append(userAttr)
|
||||
break
|
||||
user[up] = filterAttrList
|
||||
|
||||
def _formatLanguagesList(propertyValue, delimiter):
|
||||
languages = []
|
||||
for language in propertyValue:
|
||||
@@ -46788,6 +46889,7 @@ def infoUsers(entityList):
|
||||
fieldsList = []
|
||||
groups = []
|
||||
memberships = []
|
||||
userMultiAttributeFilters = {}
|
||||
skus = SKU.getAllSKUs() if not GM.Globals[GM.LICENSE_SKUS] else GM.Globals[GM.LICENSE_SKUS]
|
||||
while Cmd.ArgumentsRemaining():
|
||||
myarg = getArgument()
|
||||
@@ -46822,6 +46924,8 @@ def infoUsers(entityList):
|
||||
getGroups = getLicenses = False
|
||||
elif getFieldsList(myarg, USER_FIELDS_CHOICE_MAP, fieldsList):
|
||||
pass
|
||||
elif myarg in {'filtermultiattrtype', 'filtermultiattrcustom'}:
|
||||
_getUserMultiAttributeFilters(myarg, userMultiAttributeFilters)
|
||||
# Ignore info group arguments that may have come from whatis
|
||||
elif myarg in INFO_GROUP_OPTIONS:
|
||||
pass
|
||||
@@ -46849,6 +46953,8 @@ def infoUsers(entityList):
|
||||
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND],
|
||||
userKey=userEmail, projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
|
||||
viewType=viewType, fields=fields)
|
||||
if userMultiAttributeFilters:
|
||||
_filterUserMultiAttributes(user, userMultiAttributeFilters)
|
||||
groups = []
|
||||
memberships = []
|
||||
if getGroups or getGroupsTree:
|
||||
@@ -47286,6 +47392,8 @@ def doPrintUsers(entityList=None):
|
||||
return
|
||||
if showValidColumn:
|
||||
userEntity[showValidColumn] = True
|
||||
if userMultiAttributeFilters:
|
||||
_filterUserMultiAttributes(userEntity, userMultiAttributeFilters)
|
||||
userEmail = userEntity['primaryEmail']
|
||||
if printOptions['emailParts']:
|
||||
if userEmail.find('@') != -1:
|
||||
@@ -47428,6 +47536,7 @@ def doPrintUsers(entityList=None):
|
||||
showItemCountOnly = False
|
||||
addCSVData = {}
|
||||
includeCSVDataInJSON = False
|
||||
userMultiAttributeFilters = {}
|
||||
while Cmd.ArgumentsRemaining():
|
||||
myarg = getArgument()
|
||||
if myarg == 'todrive':
|
||||
@@ -47507,6 +47616,8 @@ def doPrintUsers(entityList=None):
|
||||
getAddCSVData(addCSVData)
|
||||
elif myarg == 'includecsvdatainjson':
|
||||
includeCSVDataInJSON = getBoolean()
|
||||
elif myarg in {'filtermultiattrtype', 'filtermultiattrcustom'}:
|
||||
_getUserMultiAttributeFilters(myarg, userMultiAttributeFilters)
|
||||
else:
|
||||
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
||||
_, _, entityList = getEntityArgument(entityList)
|
||||
@@ -58984,7 +59095,7 @@ SIZE_FIELD_CHOICE_MAP = {
|
||||
# [excludetrashed]
|
||||
# [maxfiles <Integer>] [nodataheaders <String>]
|
||||
# [countsonly [summary none|only|plus] [summaryuser <String>]
|
||||
# [showsource] [showsize] [showmimetypesize]]
|
||||
# [showsource] [showsize] [showsizeunits] [showmimetypesize]]
|
||||
# [countsrowfilter]
|
||||
# [filepath|fullpath [folderpathonly [<Boolean>]] [pathdelimiter <Character>] [addpathstojson] [showdepth]] [buildtree]
|
||||
# [allfields|<DriveFieldName>*|(fields <DriveFieldNameList>)]
|
||||
@@ -59011,7 +59122,7 @@ def printFileList(users):
|
||||
if 'mimeType' not in DFF.fieldsList:
|
||||
DFF.fieldsList.append('mimeType')
|
||||
skipObjects.discard(sizeField)
|
||||
if showSize and sizeField not in DFF.fieldsList:
|
||||
if (showSize or showSizeUnits) and sizeField not in DFF.fieldsList:
|
||||
DFF.fieldsList.append(sizeField)
|
||||
if (DLP.minimumFileSize is not None) or (DLP.maximumFileSize is not None):
|
||||
_setSkipObjects(skipObjects, [sizeField], DFF.fieldsList)
|
||||
@@ -59209,6 +59320,8 @@ def printFileList(users):
|
||||
row['Name'] = sourceName
|
||||
if showSize:
|
||||
row['Size'] = sizeTotal
|
||||
if showSizeUnits:
|
||||
row['SizeUnits'] = formatFileSize(sizeTotal)
|
||||
if addCSVData:
|
||||
row.update(addCSVData)
|
||||
for mimeType, mtinfo in sorted(mimeTypeInfo.items()):
|
||||
@@ -59225,7 +59338,7 @@ def printFileList(users):
|
||||
FJQC = FormatJSONQuoteChar(csvPF)
|
||||
addPathsToJSON = continueOnInvalidQuery = countsRowFilter = buildTree = countsOnly = filepath = fullpath = folderPathOnly = \
|
||||
getPermissionsForSharedDrives = mimeTypeInQuery = noRecursion = oneItemPerRow = stripCRsFromName = \
|
||||
showParentsIdsAsList = showDepth = showParent = showSize = showMimeTypeSize = showSource = False
|
||||
showParentsIdsAsList = showDepth = showParent = showSize = showSizeUnits = showMimeTypeSize = showSource = False
|
||||
sizeField = 'quotaBytesUsed'
|
||||
pathDelimiter = '/'
|
||||
pmselect = True
|
||||
@@ -59298,22 +59411,12 @@ def printFileList(users):
|
||||
summaryUser = getString(Cmd.OB_STRING)
|
||||
elif myarg == 'showsource':
|
||||
showSource = True
|
||||
if countsOnly:
|
||||
if not showSize:
|
||||
csvPFco.SetTitles(['Owner', 'Source', 'Name', 'Total'])
|
||||
else:
|
||||
csvPFco.SetTitles(['Owner', 'Source', 'Name', 'Total', 'Size'])
|
||||
csvPFco.SetSortAllTitles()
|
||||
elif myarg in {'showsize', 'showmimetypesize'}:
|
||||
elif myarg == 'showsize':
|
||||
showSize = True
|
||||
if countsOnly:
|
||||
if not showSource:
|
||||
csvPFco.SetTitles(['Owner', 'Total', 'Size'])
|
||||
else:
|
||||
csvPFco.SetTitles(['Owner', 'Source', 'Name', 'Total', 'Size'])
|
||||
csvPFco.SetSortAllTitles()
|
||||
if myarg == 'showmimetypesize':
|
||||
showMimeTypeSize = True
|
||||
elif myarg == 'showsizeunits':
|
||||
showSizeUnits = True
|
||||
elif myarg == 'showmimetypesize':
|
||||
showMimeTypeSize = showSize = True
|
||||
elif myarg == 'sizefield':
|
||||
sizeField = getChoice(SIZE_FIELD_CHOICE_MAP, mapChoice=True)
|
||||
elif myarg == 'delimiter':
|
||||
@@ -59344,6 +59447,15 @@ def printFileList(users):
|
||||
continueOnInvalidQuery = getBoolean()
|
||||
else:
|
||||
FJQC.GetFormatJSONQuoteChar(myarg)
|
||||
if countsOnly:
|
||||
titles = ['Owner', 'Total'] if not showSource else ['Owner', 'Source', 'Name', 'Total']
|
||||
if showSize:
|
||||
titles.append('Size')
|
||||
if showSizeUnits:
|
||||
titles.append('SizeUnits')
|
||||
csvPFco.SetTitles(titles)
|
||||
csvPFco.SetSortAllTitles()
|
||||
|
||||
if not filepath and not fullpath:
|
||||
showDepth = False
|
||||
noSelect = noFileSelectFileIdEntity(fileIdEntity)
|
||||
@@ -59409,9 +59521,11 @@ def printFileList(users):
|
||||
csvPF.AddJSONTitles(sorted(addCSVData.keys()))
|
||||
else:
|
||||
csvPFco.AddTitles(sorted(addCSVData.keys()))
|
||||
csvPFco.MoveTitlesToEnd(['Total'])
|
||||
if showSize:
|
||||
csvPFco.MoveTitlesToEnd(['Size'])
|
||||
csvPFco.MoveTitlesToEnd(['Total'])
|
||||
if showSizeUnits:
|
||||
csvPFco.MoveTitlesToEnd(['SizeUnits'])
|
||||
csvPFco.SetSortAllTitles()
|
||||
if filepath and not countsOnly:
|
||||
csvPF.AddTitles('paths')
|
||||
@@ -60119,7 +60233,7 @@ def _updateLastModificationRow(row, lastModification):
|
||||
# [filenamematchpattern <REMatchPattern>]
|
||||
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
||||
# [excludetrashed] (addcsvdata <FieldName> <String>)*
|
||||
# [showsize] [showmimetypesize]
|
||||
# [showsize] [showsizeunits] [showmimetypesize]
|
||||
# [showlastmodification] [pathdelimiter <Character>]
|
||||
# (addcsvdata <FieldName> <String>)*
|
||||
# [summary none|only|plus] [summaryuser <String>]
|
||||
@@ -60134,14 +60248,14 @@ def _updateLastModificationRow(row, lastModification):
|
||||
# [filenamematchpattern <REMatchPattern>]
|
||||
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
||||
# [excludetrashed]
|
||||
# [showsize] [showmimetypesize]
|
||||
# [showsize] [showsizeunits] [showmimetypesize]
|
||||
# [showlastmodification] [pathdelimiter <Character>]
|
||||
# [summary none|only|plus] [summaryuser <String>]
|
||||
def printShowFileCounts(users):
|
||||
def _setSelectionFields():
|
||||
if DLP.showOwnedBy is not None:
|
||||
fieldsList.extend(OWNED_BY_ME_FIELDS_TITLES)
|
||||
if showSize or (DLP.minimumFileSize is not None) or (DLP.maximumFileSize is not None):
|
||||
if (showSize or showSizeUnits) or (DLP.minimumFileSize is not None) or (DLP.maximumFileSize is not None):
|
||||
fieldsList.append(sizeField)
|
||||
if showLastModification:
|
||||
fieldsList.extend(['id,name,modifiedTime,lastModifyingUser(me, displayName, emailAddress),parents'])
|
||||
@@ -60174,7 +60288,9 @@ def printShowFileCounts(users):
|
||||
kvList = [Ent.USER, user]
|
||||
dataList = [Ent.Choose(Ent.DRIVE_FILE_OR_FOLDER, countTotal), countTotal]
|
||||
if showSize:
|
||||
dataList.extend([Ent.Singular(Ent.SIZE), sizeTotal])
|
||||
dataList.extend(['Size', sizeTotal])
|
||||
if showSizeUnits:
|
||||
dataList.extend(['SizeUnits', formatFileSize(sizeTotal)])
|
||||
if sharedDriveId:
|
||||
dataList.extend(['Item cap', f"{countTotal/SHARED_DRIVE_MAX_FILES_FOLDERS:.2%}"])
|
||||
printEntityKVList(kvList, dataList, i, count)
|
||||
@@ -60194,6 +60310,8 @@ def printShowFileCounts(users):
|
||||
row = {'User': user, 'Total': countTotal}
|
||||
if showSize:
|
||||
row['Size'] = sizeTotal
|
||||
if showSizeUnits:
|
||||
row['SizeUnits'] = formatFileSize(sizeTotal)
|
||||
if showLastModification:
|
||||
_updateLastModificationRow(row, lastModification)
|
||||
if addCSVData:
|
||||
@@ -60211,7 +60329,7 @@ def printShowFileCounts(users):
|
||||
DLP = DriveListParameters({'allowChoose': False, 'allowCorpora': True, 'allowQuery': True, 'mimeTypeInQuery': True})
|
||||
pathDelimiter = '/'
|
||||
sharedDriveId = sharedDriveName = ''
|
||||
continueOnInvalidQuery = showSize = showLastModification = showMimeTypeSize = False
|
||||
continueOnInvalidQuery = showSize = showSizeUnits = showLastModification = showMimeTypeSize = False
|
||||
sizeField = 'quotaBytesUsed'
|
||||
summary = FILECOUNT_SUMMARY_NONE
|
||||
summaryUser = FILECOUNT_SUMMARY_USER
|
||||
@@ -60231,12 +60349,14 @@ def printShowFileCounts(users):
|
||||
fileIdEntity = getSharedDriveEntity()
|
||||
elif myarg == 'showsize':
|
||||
showSize = True
|
||||
elif myarg == 'showsizeunits':
|
||||
showSizeUnits = True
|
||||
elif myarg == 'showmimetypesize':
|
||||
showMimeTypeSize = showSize = True
|
||||
elif myarg == 'sizefield':
|
||||
sizeField = getChoice(SIZE_FIELD_CHOICE_MAP, mapChoice=True)
|
||||
elif myarg == 'showlastmodification':
|
||||
showLastModification = True
|
||||
elif myarg == 'showmimetypesize':
|
||||
showMimeTypeSize = showSize = True
|
||||
elif myarg == 'summary':
|
||||
summary = getChoice(FILECOUNT_SUMMARY_CHOICE_MAP, mapChoice=True)
|
||||
elif myarg == 'summaryuser':
|
||||
@@ -60265,6 +60385,8 @@ def printShowFileCounts(users):
|
||||
_setSelectionFields()
|
||||
if csvPF:
|
||||
sortTitles = ['User', 'id', 'name', 'Total', 'Item cap'] if fileIdEntity.get('shareddrive') else ['User', 'Total']
|
||||
if showSizeUnits:
|
||||
sortTitles.insert(sortTitles.index('Total')+1, 'SizeUnits')
|
||||
if showSize:
|
||||
sortTitles.insert(sortTitles.index('Total')+1, 'Size')
|
||||
if showLastModification:
|
||||
|
||||
@@ -1,3 +1,72 @@
|
||||
#
|
||||
# This is a custom certificate authority bundle for GAM
|
||||
# It's composed of Let's Encrypt Root CAs and Google's
|
||||
# certificate bundle. This should be the minimal list of
|
||||
# CAs required to talk to Google and Github.
|
||||
|
||||
# Operating CA: Let's Encrypt
|
||||
# Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X1
|
||||
# Subject: C = US, O = Internet Security Research Group, CN = ISRG Root X1
|
||||
# Label: "ISRG Root X1"
|
||||
# Serial: 172886928669790476064670243504169061120
|
||||
# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e
|
||||
# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8
|
||||
# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
||||
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
||||
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
||||
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
||||
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
||||
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
||||
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
||||
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
||||
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
||||
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
||||
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
||||
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
||||
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
||||
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
||||
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
||||
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
||||
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
||||
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
||||
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
||||
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
||||
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
||||
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
||||
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
||||
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
||||
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
||||
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Operating CA: Let's Encrypt
|
||||
# Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X2
|
||||
# Subject: C = US, O = Internet Security Research Group, CN = ISRG Root X2
|
||||
# Label: "ISRG Root X2"
|
||||
# Serial: 87493402998870891108772069816698636114
|
||||
# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5
|
||||
# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af
|
||||
# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
|
||||
CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
|
||||
R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
|
||||
MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
|
||||
ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
|
||||
EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
|
||||
+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
|
||||
ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
|
||||
AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
|
||||
zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
|
||||
tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
|
||||
/q4AaOeMSQ+2b1tbFfLn
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Operating CA: DigiCert
|
||||
# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
|
||||
# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
|
||||
|
||||
@@ -1,128 +1,147 @@
|
||||
// Node.js script that implements an Appium client which will launch
|
||||
// Simply Sign Desktop app and log a user in. Once logged in it should
|
||||
// be possible to use tools like signtool.exe to sign Windows EXE/MSI files
|
||||
// with the Certum certificate.
|
||||
// Node.js script to launch Simply Sign Desktop app and log a user in
|
||||
// using native Windows keystrokes and screenshot-desktop for reliable CI imaging.
|
||||
|
||||
import { Key, remote } from 'webdriverio';
|
||||
import { exec } from 'child_process';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import { TOTP } from 'totp-generator';
|
||||
|
||||
async function screenshot(driver, filename) {
|
||||
// uncomment to save .png screenshots
|
||||
await driver.saveScreenshot(filename);
|
||||
return
|
||||
}
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function executeCommand(command) {
|
||||
// Native PowerShell Keystroke Sender
|
||||
function sendKeys(keys) {
|
||||
const script = `$wshell = New-Object -ComObject wscript.shell; $wshell.SendKeys('${keys}')`;
|
||||
execSync(`powershell -Command "${script}"`);
|
||||
}
|
||||
|
||||
// Native PowerShell Desktop Clear
|
||||
function minimizeAllWindows() {
|
||||
console.log('Minimizing all rogue background windows...');
|
||||
const script = `$shell = New-Object -ComObject "Shell.Application"; $shell.MinimizeAll()`;
|
||||
try {
|
||||
let { stdout, stderr } = await exec(command);
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
console.error(`Error executing command: ${command}`);
|
||||
console.error(`Error details: ${error}`);
|
||||
throw error;
|
||||
execSync(`powershell -Command "${script}"`);
|
||||
} catch (err) {
|
||||
console.log('Minimize command failed silently.');
|
||||
}
|
||||
}
|
||||
|
||||
async function runSSD() {
|
||||
const opts = {
|
||||
port: 4723,
|
||||
logLevel: "silent",
|
||||
capabilities: {
|
||||
platformName: "Windows",
|
||||
"appium:app": "C:\\Program Files\\Certum\\SimplySign Desktop\\SimplySignDesktop.exe",
|
||||
"appium:automationName": "Windows",
|
||||
},
|
||||
};
|
||||
async function takeScreenshot(filename) {
|
||||
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||
const fullPath = path.join(workspace, filename);
|
||||
|
||||
let driver;
|
||||
try {
|
||||
driver = await remote(opts);
|
||||
|
||||
// Github Actions Win ARM64 is stuck on a OOB screen that steals focus
|
||||
// These enter / escapes should dismiss it.
|
||||
const runner_arch = process.env.RUNNER_ARCH;
|
||||
if ( runner_arch === "ARM64" ) {
|
||||
console.log('Running on ARM64...');
|
||||
await sleep(3000); // Pause execution for 3 seconds
|
||||
await screenshot(driver, 'oob1.png');
|
||||
await driver.sendKeys([Key.Enter]);
|
||||
await sleep(3000); // Pause execution for 3 seconds
|
||||
await screenshot(driver, 'oob2.png');
|
||||
await driver.sendKeys([Key.Enter]);
|
||||
await sleep(3000); // Pause execution for 3 seconds
|
||||
await screenshot(driver, 'oob3.png');
|
||||
await driver.sendKeys([Key.Escape]);
|
||||
await screenshot(driver, 'oob6.png');
|
||||
} else {
|
||||
console.log('NOT running on ARM64');
|
||||
}
|
||||
|
||||
// Execute SSD again to open login dialog
|
||||
exec('"C:\\Program Files\\Certum\\SimplySign Desktop\\SimplySignDesktop.exe"', (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`exec error: ${error}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Login
|
||||
const windows = await driver.getWindowHandles();
|
||||
const login_window = windows[0]
|
||||
await driver.switchWindow(login_window);
|
||||
await screenshot(driver, 'login01.png');
|
||||
const id_value = 'jay0lee@gmail.com';
|
||||
const id_arr = [...id_value];
|
||||
await driver.sendKeys(id_arr);
|
||||
await screenshot(driver, 'login02.png');
|
||||
await driver.sendKeys([Key.Tab]);
|
||||
console.log('Our secret is ' + process.env.TOTP_SECRET.length + ' characters.');
|
||||
// We wait until the last possible second to generate
|
||||
// our TOTP to ensure it's still valid.
|
||||
const { otp } = await TOTP.generate(process.env.TOTP_SECRET, {algorithm: 'SHA-256'});
|
||||
console.log('Our token is ' + otp.length + ' characters.');
|
||||
const otp_arr = [...otp];
|
||||
await driver.sendKeys(otp_arr);
|
||||
await screenshot(driver, 'login03.png');
|
||||
await driver.sendKeys([Key.Enter]);
|
||||
|
||||
// TODO: it's expected that on successful login the window
|
||||
// will close and these screenshots will error out. Figure
|
||||
// out how to handle that gracefully.
|
||||
await screenshot(driver, 'login04.png');
|
||||
await sleep(500);
|
||||
await screenshot(driver, 'login05.png');
|
||||
await sleep(500);
|
||||
await screenshot(driver, 'login06.png');
|
||||
await sleep(500);
|
||||
await screenshot(driver, 'login07.png');
|
||||
await sleep(500);
|
||||
await screenshot(driver, 'login08.png');
|
||||
await sleep(500);
|
||||
await screenshot(driver, 'login09.png');
|
||||
await sleep(500);
|
||||
await screenshot(driver, 'login10.png');
|
||||
await sleep(500);
|
||||
await screenshot(driver, 'login11.png');
|
||||
await sleep(500);
|
||||
await screenshot(driver, 'login12.png');
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
//console.error("Error during Appium run:");
|
||||
const psScript = `
|
||||
Add-Type -AssemblyName System.Windows.Forms;
|
||||
Add-Type -AssemblyName System.Drawing;
|
||||
$Screen = [System.Windows.Forms.SystemInformation]::VirtualScreen;
|
||||
|
||||
if ($Screen.Width -eq 0 -or $Screen.Height -eq 0) {
|
||||
Write-Error "Screen dimensions are 0x0. Desktop not fully initialized.";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
$bitmap = New-Object System.Drawing.Bitmap $Screen.Width, $Screen.Height;
|
||||
$graphic = [System.Drawing.Graphics]::FromImage($bitmap);
|
||||
$graphic.CopyFromScreen($Screen.Left, $Screen.Top, 0, 0, $bitmap.Size);
|
||||
$bitmap.Save('${fullPath}');
|
||||
`;
|
||||
|
||||
try {
|
||||
execSync(`powershell -Command "${psScript}"`);
|
||||
console.log(`Saved screenshot: ${fullPath}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to save screenshot ${fullPath}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// INTENTIONAL Keep driver open so tray icon for Certum doesn't close
|
||||
// finally {
|
||||
// if (driver) {
|
||||
// await driver.deleteSession(); // Close the Appium session
|
||||
// }
|
||||
//}
|
||||
// Fire and forget application launcher
|
||||
function launchSSD() {
|
||||
const child = spawn('C:\\Program Files\\Certum\\SimplySign Desktop\\SimplySignDesktop.exe', [], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
child.unref();
|
||||
}
|
||||
|
||||
async function runSSD() {
|
||||
await takeScreenshot('001.png');
|
||||
minimizeAllWindows();
|
||||
await sleep(2000);
|
||||
await takeScreenshot('002.png');
|
||||
sendKeys('{ESC}');
|
||||
await sleep(2000);
|
||||
await takeScreenshot('003.png');
|
||||
//sendKeys('{ESC}');
|
||||
//await sleep(2000);
|
||||
//await takeScreenshot('004.png');
|
||||
//sendKeys('{ESC}');
|
||||
//await sleep(2000);
|
||||
//await takeScreenshot('005.png');
|
||||
//sendKeys('%{F4}');
|
||||
//await sleep(2000);
|
||||
//await takeScreenshot('006.png');
|
||||
//sendKeys('%{F4}');
|
||||
//await sleep(2000);
|
||||
//await takeScreenshot('007.png');
|
||||
|
||||
// Re-execute SSD to open login dialog
|
||||
launchSSD();
|
||||
await sleep(3000);
|
||||
await takeScreenshot('008.png');
|
||||
launchSSD();
|
||||
await sleep(3000);
|
||||
await takeScreenshot('009.png');
|
||||
|
||||
// 2. Login Flow
|
||||
console.log('Typing credentials...');
|
||||
|
||||
// Type Email
|
||||
sendKeys('jay0lee@gmail.com');
|
||||
await sleep(500);
|
||||
await takeScreenshot('010.png');
|
||||
|
||||
// Tab to next field
|
||||
sendKeys('{TAB}');
|
||||
await sleep(500);
|
||||
|
||||
// Generate and type TOTP
|
||||
console.log(`Our secret is ${process.env.TOTP_SECRET.length} characters.`);
|
||||
const { otp } = await TOTP.generate(process.env.TOTP_SECRET, {algorithm: 'SHA-256'});
|
||||
console.log(`Our token is ${otp.length} characters.`);
|
||||
|
||||
sendKeys(otp);
|
||||
await sleep(500);
|
||||
await takeScreenshot('011.png');
|
||||
|
||||
// Submit
|
||||
sendKeys('{ENTER}');
|
||||
console.log('Login sequence complete.');
|
||||
|
||||
// Screenshot cascade to monitor the window closing
|
||||
await takeScreenshot('012.png');
|
||||
await sleep(500);
|
||||
await takeScreenshot('013.png');
|
||||
await sleep(500);
|
||||
await takeScreenshot('014.png');
|
||||
await sleep(500);
|
||||
|
||||
|
||||
console.log('Exiting script, leaving SimplySign running in background.');
|
||||
|
||||
// Verification block to list all PNGs in the workspace
|
||||
console.log('\n--- Screenshot Verification ---');
|
||||
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||
try {
|
||||
const files = fs.readdirSync(workspace);
|
||||
const pngFiles = files.filter(f => f.endsWith('.png'));
|
||||
console.log(`Target Directory: ${workspace}`);
|
||||
console.log(`Found ${pngFiles.length} .png files:`);
|
||||
pngFiles.forEach(f => console.log(` - ${f}`));
|
||||
} catch (err) {
|
||||
console.error(`Error reading directory ${workspace}:`, err.message);
|
||||
}
|
||||
console.log('-------------------------------\n');
|
||||
}
|
||||
|
||||
runSSD();
|
||||
|
||||
@@ -58,7 +58,7 @@ See: https://cloud.google.com/identity/docs/concepts/supported-policy-api-settin
|
||||
Display selected policies.
|
||||
```
|
||||
gam info policies <CIPolicyEntity>
|
||||
[nowarnings] [noappnames]
|
||||
[nowarnings] [noappnames] [noidmappimg]
|
||||
[formatjson]
|
||||
```
|
||||
|
||||
@@ -69,16 +69,20 @@ Select policies::
|
||||
|
||||
By default, policy warnings are displayed, use the 'nowarnings` option to suppress their display.
|
||||
|
||||
By default, additional API calls are made for `settings/workspace_marketplace.apps_allowlist`
|
||||
By default, additional API calls are made for `settings/workspace_marketplace.apps_allowlist`
|
||||
to get the application name for the application ID. Use option `noappnames` to suppress these calls.
|
||||
|
||||
By default, additional API calls are made to add the `policyQuery/groupEmail` and `policyQuery/orgUnitPath` fields
|
||||
that are mapped from the `policyQuery/group` and `policyQuery/orgUnit` fields. Use option `noidmapping'
|
||||
to suppress these calls and not add the additional fields.
|
||||
|
||||
By default, Gam displays the information as an indented list of keys and values.
|
||||
* `formatjson` - Display the fields in JSON format.
|
||||
|
||||
Display all or filtered policies.
|
||||
```
|
||||
gam show policies
|
||||
[filter <String>] [nowarnings] [noappnames]
|
||||
[filter <String>] [nowarnings] [noappnames] [noidmappimg]
|
||||
[group <REMatchPattern>] [ou|org|orgunit <REMatchPattern>]
|
||||
[formatjson]
|
||||
```
|
||||
@@ -92,12 +96,16 @@ By default, policy warnings are displayed, use the `nowarnings` option to suppre
|
||||
By default, additional API calls are made for `settings/workspace_marketplace.apps_allowlist`
|
||||
to get the application name for the application ID. Use option `noappnames` to suppress these calls.
|
||||
|
||||
By default, additional API calls are made to add the `policyQuery/groupEmail` and `policyQuery/orgUnitPath` fields
|
||||
that are mapped from the `policyQuery/group` and `policyQuery/orgUnit` fields. Use option `noidmapping'
|
||||
to suppress these calls and not add the additional fields.
|
||||
|
||||
By default, Gam displays the information as an indented list of keys and values.
|
||||
* `formatjson` - Display the fields in JSON format.
|
||||
|
||||
```
|
||||
gam print policies [todrive <ToDriveAttribute>*]
|
||||
[filter <String>] [nowarnings] [noappnames]
|
||||
[filter <String>] [nowarnings] [noappnames] [noidmappimg]
|
||||
[group <REMatchPattern>] [ou|org|orgunit <REMatchPattern>]
|
||||
[formatjson [quotechar <Character>]]
|
||||
```
|
||||
@@ -108,6 +116,10 @@ By default, all policies are displayed:
|
||||
|
||||
By default, policy warnings are displayed, use the `nowarnings` option to suppress their display.
|
||||
|
||||
By default, additional API calls are made to add the `policyQuery/groupEmail` and `policyQuery/orgUnitPath` fields
|
||||
that are mapped from the `policyQuery/group` and `policyQuery/orgUnit` fields. Use option `noidmapping'
|
||||
to suppress these calls and not add the additional fields.
|
||||
|
||||
By default, additional API calls are made for `settings/workspace_marketplace.apps_allowlist`
|
||||
to get the application name for the application ID. Use option `noappnames` to suppress these calls.
|
||||
|
||||
|
||||
@@ -25,41 +25,41 @@ start a new terminal session and reissue the command from above.
|
||||
## Executable, Manual
|
||||
|
||||
* Executable Archive, Manual, Linux/Google Cloud Shell
|
||||
- `gam-7.wx.yz-linux-x86_64-glibc2.36.tar.xz`
|
||||
- `gam-7.wx.yz-linux-x86_64-glibc2.35.tar.xz`
|
||||
- `gam-7.wx.yz-linux-x86_64-glibc2.39.tar.xz`
|
||||
- `gam-7.wx.yz-linux-x86_64-legacy.tar.xz`
|
||||
- Download the archive, extract the contents into some directory.
|
||||
- Start a terminal session.
|
||||
|
||||
* Executable Archive, Manual, Raspberry Pi/ChromeOS ARM devices
|
||||
- `gam-7.wx.yz-linux-arm64-glibc2.36.tar.xz`
|
||||
- `gam-7.wx.yz-linux-arm64-glibc2.35.tar.xz`
|
||||
- `gam-7.wx.yz-linux-arm64-glibc2.39.tar.xz`
|
||||
- `gam-7.wx.yz-linux-arm64-legacy.tar.xz`
|
||||
- Download the archive, extract the contents into some directory.
|
||||
- Start a terminal session.
|
||||
|
||||
* Executable Archive, Manual, Mac OS versions Sonoma, Sequoia - M1/M2
|
||||
- `gam-7.wx.yz-macos14.7-arm64.tar.xz`
|
||||
- `gam-7.wx.yz-macos14.8-arm64.tar.xz`
|
||||
- Download the archive, extract the contents into some directory.
|
||||
- Start a terminal session.
|
||||
|
||||
* Executable Archive, Manual, Mac OS versions Sequoia - M2/M3
|
||||
- `gam-7.wx.yz-macos15.6-arm64.tar.xz`
|
||||
- Download the archive, extract the contents into some directory.
|
||||
- Start a terminal session.
|
||||
|
||||
* Executable Archive, Manual, Mac OS versions Tahoe - M2/M3/M4
|
||||
- `gam-7.wx.yz-macos26.0-arm64.tar.xz`
|
||||
- Download the archive, extract the contents into some directory.
|
||||
- Start a terminal session.
|
||||
|
||||
* Executable Archive, Manual, Mac OS, versions Ventura, Sonoma - Intel
|
||||
- `gam-7.wx.yz-macos13.7-x86_64.tar.xz`
|
||||
- `gam-7.wx.yz-macos15.7-arm64.tar.xz`
|
||||
- Download the archive, extract the contents into some directory.
|
||||
- Start a terminal session.
|
||||
|
||||
* Executable Archive, Manual, Mac OS, versions Sequoia, Tahoe - Intel
|
||||
- `gam-7.wx.yz-macos15.6-x86_64.tar.xz`
|
||||
- `gam-7.wx.yz-macos15.7-x86_64.tar.xz`
|
||||
- Download the archive, extract the contents into some directory.
|
||||
- Start a terminal session.
|
||||
|
||||
* Executable Archive, Manual, Mac OS versions Tahoe - M2/M3/M4
|
||||
- `gam-7.wx.yz-macos26.3-arm64.tar.xz`
|
||||
- Download the archive, extract the contents into some directory.
|
||||
- Start a terminal session.
|
||||
|
||||
* Executable Archive, Manual, Mac OS versions Tahoe - Intel
|
||||
- `gam-7.wx.yz-macos26.3-x86_64.tar.xz`
|
||||
- Download the archive, extract the contents into some directory.
|
||||
- Start a terminal session.
|
||||
|
||||
@@ -69,7 +69,7 @@ start a new terminal session and reissue the command from above.
|
||||
- Start a Command Prompt/PowerShell session.
|
||||
|
||||
* Executable Installer, Manual, Windows 64 bit
|
||||
- `gam-7.wx.yz-windows-x86_64.msi`
|
||||
- `gam-7.wx.yz-windows-x86_64.exe`
|
||||
- Download the installer and run it.
|
||||
- Start a Command Prompt/PowerShell session.
|
||||
|
||||
@@ -79,7 +79,7 @@ start a new terminal session and reissue the command from above.
|
||||
- Start a Command Prompt/PowerShell session.
|
||||
|
||||
* Executable Installer, Manual, Windows 11 ARM
|
||||
- `gam-7.wx.yz-windows-arm64.msi`
|
||||
- `gam-7.wx.yz-windows-arm64.exe`
|
||||
- Download the installer and run it.
|
||||
- Start a Command Prompt/PowerShell session.
|
||||
|
||||
|
||||
@@ -10,6 +10,89 @@ Add the `-s` option to the end of the above commands to suppress creating the `g
|
||||
|
||||
See [Downloads-Installs-GAM7](https://github.com/GAM-team/GAM/wiki/Downloads-Installs) for Windows or other options, including manual installation
|
||||
|
||||
### 7.36.02
|
||||
|
||||
Added option `threadid <String>` to `gam [<UserTypeEntity>] sendemail` that causes Gmail to recognize the message
|
||||
in conversation mode in for the user sending the message.
|
||||
|
||||
* See: https://github.com/GAM-team/GAM/wiki/Send-Email#conversation-mode
|
||||
|
||||
### 7.36.01
|
||||
|
||||
Fixed bug in `gam info|print|show policies` where the `policyQuery/query` field was not displayed.
|
||||
|
||||
Added option `noidmapping` to `gam info|print|show policies` to suppress adding the `policyQuery/groupEmail` and
|
||||
`policyQuery/orgUnitPath` name fields that are mapped from the `policyQuery/group` and `policyQuery/orgInit` id fields.
|
||||
|
||||
### 7.36.00
|
||||
|
||||
Added options `filtermultiattrtype` and filtermultiattrcustom` to `gam info user` and
|
||||
`gam print users` that support filtering `<UserMultiAttribute>` display based on `type` or `customType`.
|
||||
|
||||
```
|
||||
<UserMultiAttributeFilterName> ::=
|
||||
address|addresses|
|
||||
externalid|externalids|
|
||||
im|ims|
|
||||
keyword|keywords|
|
||||
location|locations|
|
||||
orgainzation|organizations|
|
||||
otheremail|otheremails|
|
||||
phone|phones|
|
||||
relation|relations|
|
||||
website|websites
|
||||
```
|
||||
|
||||
* `filtermultiattrtype <UserMultiAttributeFilterName> <String>` - Display `<UserMultiAttributeFilterName>` if its `type` is `<String>`
|
||||
* `filtermultiattrcustom <UserMultiAttributeFilterName> <String>` - Display `<UserMultiAttributeFilterName>` if its `customType` is `<String>`
|
||||
|
||||
```
|
||||
gam info user user@domain.com quick filtermultiattrtype organizations work filtermultiattrcustom phones private
|
||||
```
|
||||
|
||||
### 7.35.03
|
||||
|
||||
Updated `gam <UserTypeEntity> print filelist|filecounts` to handle options `showsize` and `showsizeunits` as independent options.
|
||||
* `showsize` - Display a column `Size` with a byte count
|
||||
* `showsizeunits` - Display a column `SizeUnits` with a formatted size with units
|
||||
|
||||
If you select both options, you can sort multiple rows using the `Size` column.
|
||||
|
||||
### 7.35.02
|
||||
|
||||
Added option `showsizeunits` to `gam gam <UserTypeEntity> print filelist|filecounts` as an alternative to option `showsize`.
|
||||
* `showsize` - 31549200951 - This is a byte count
|
||||
* `showsizeunits` - 31.55 GB - This is as shown in the Admin console
|
||||
|
||||
### 7.35.01
|
||||
|
||||
The following commands have been updated to not verify the existence of `gam.cfg` credentials files
|
||||
as the WARNING messages about the missing files can be confusing to new users setting up GAM.
|
||||
```
|
||||
gam checkconn
|
||||
gam oauth|oauth2
|
||||
gam version
|
||||
```
|
||||
|
||||
### 7.35.00
|
||||
|
||||
Windows `gam-7.wx.yz-x86_64.msi` has been replaced with `gam-7.wx.yz-x86_64.exe`.
|
||||
|
||||
Windows `gam-7.wx.yz-arm64.msi` has been replaced with `gam-7.wx.yz-arm64.exe`.
|
||||
|
||||
Updated cacerts.pem to avoid to following error in `gam checkconn`.
|
||||
```
|
||||
Checking raw.githubusercontent.com (185.199.110.133) (2)... ERROR
|
||||
Certificate verification failed. If you are behind a firewall / proxy server that does TLS / SSL inspection you may need to point GAM at your certificate authority file by setting cacerts_pem = /path/to/your/certauth.pem in gam.cfg.
|
||||
```
|
||||
|
||||
If you have customized cacerts.pem, update your version with the `Operating CA: Let's Encrypt` values from the GAM default version.
|
||||
|
||||
### 7.34.13
|
||||
|
||||
Fixed bug in `gam info policies <CIPolicyNameEntity> ... formatjson` where extraneous line
|
||||
`Show Info 1 Policy` was displayed.
|
||||
|
||||
### 7.34.12
|
||||
|
||||
Fixed build errors that prevented Windows zip files from being created.
|
||||
|
||||
@@ -129,7 +129,7 @@ See: [Downloads-Installs-GAM7](Downloads-Installs-GAM7)
|
||||
### Update to latest version, use current path `C:\GAMADV-XTD3`.
|
||||
You don't have to update path or scripts.
|
||||
* Executable Installer, Manual, Windows 64 bit
|
||||
- `gam-7.wx.yz-windows-x86_64.msi`
|
||||
- `gam-7.wx.yz-windows-x86_64.exe`
|
||||
- Download the installer and run it. When prompted for the Destination Foler, enter `C:\GAMADV-XTD3`.
|
||||
* Executable Archive, Manual, Windows 64 bit
|
||||
- `gam-7.wx.yz-windows-x86_64.zip`
|
||||
@@ -139,7 +139,7 @@ Your update is complete.
|
||||
|
||||
### Update to latest version, use new path `C:\GAM7`.
|
||||
* Executable Installer, Manual, Windows 64 bit
|
||||
- `gam-7.wx.yz-windows-x86_64.msi`
|
||||
- `gam-7.wx.yz-windows-x86_64.exe`
|
||||
- Download the installer and run it.
|
||||
- Start a Command Prompt/PowerShell session.
|
||||
* Executable Archive, Manual, Windows 64 bit
|
||||
|
||||
@@ -251,11 +251,10 @@ writes the credentials into the file oauth2.txt.
|
||||
```
|
||||
gamteam@server:/Users/gamteam$ rm -f /Users/gamteam/GAMConfig/oauth2.txt
|
||||
gamteam@server:/Users/gamteam$ gam version
|
||||
WARNING: Config File: /Users/gamteam/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/gamteam/GAMConfig/oauth2.txt, Not Found
|
||||
GAM 7.34.12 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM 7.36.02 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.3 64-bit final
|
||||
macOS Tahoe 26.3 arm64
|
||||
macOS Tahoe 26.3.1 arm64
|
||||
Path: /Users/gamteam/bin/gam7
|
||||
Config File: /Users/gamteam/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
|
||||
|
||||
@@ -1035,8 +1034,7 @@ writes the credentials into the file oauth2.txt.
|
||||
```
|
||||
C:\>del C:\GAMConfig\oauth2.txt
|
||||
C:\>gam version
|
||||
WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found
|
||||
GAM 7.34.12 - https://github.com/GAM-team/GAM - pythonsource
|
||||
GAM 7.36.02 - https://github.com/GAM-team/GAM - pythonsource
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.3 64-bit final
|
||||
Windows 11 10.0.26200 AMD64
|
||||
|
||||
@@ -377,6 +377,9 @@ features "CameraSet"
|
||||
features "'Laptop Cart'"
|
||||
features "CameraSet,'Laptop Cart'"
|
||||
```
|
||||
|
||||
For quoting rules, see: [List Quoting Rules](Command-Line-Parsing)
|
||||
|
||||
## Manage buildings
|
||||
When creating a building, at a minimum you must enter `address|addresslines` and `country|regioncode`.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
- [Send an email to users](#send-an-email-to-users)
|
||||
- [Simple `replace <Tag> <String>` processing](Tag-Replace)
|
||||
- [Example](#example)
|
||||
- [Conversation mode](#conversation-mode)
|
||||
|
||||
## Note
|
||||
Thanks to @bousquf for the following enhancement. You want to send a message from an authorized group
|
||||
@@ -214,14 +215,14 @@ Configure it at Admin Console > Apps > Google Workspace > Gmail > Routing > SMTP
|
||||
gam sendemail [recipient|to] <RecipientEntity>
|
||||
[from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
|
||||
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
||||
[subject <String>]
|
||||
[<MessageContent>]
|
||||
[subject <String>] [<MessageContent>]
|
||||
(replace <Tag> <String>)*
|
||||
(replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
|
||||
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
|
||||
(embedimage <FileName> <String>)*
|
||||
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
||||
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
||||
[threadid <String>]
|
||||
```
|
||||
By default, emails will be sent from the admin user named in oauth2.txt, override this with the `from <EmailAddress>` option.
|
||||
|
||||
@@ -272,14 +273,14 @@ You can specify additional recipients, e.g., help desk personnel.
|
||||
gam sendemail [recipient|to] <RecipientEntity> [from <EmailAddress>]
|
||||
[replyto <EmailAddress>]
|
||||
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
||||
[subject <String>]
|
||||
[<MessageContent>]
|
||||
[subject <String>] [<MessageContent>]
|
||||
(replace <Tag> <String>)*
|
||||
(replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
|
||||
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
|
||||
(embedimage <FileName> <String>)*
|
||||
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
||||
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
||||
[threadid <String>]
|
||||
```
|
||||
|
||||
By default, emails will be sent from the admin user named in oauth2.txt, override this with the `from <EmailAddress>` option.
|
||||
@@ -353,14 +354,14 @@ gam csv Users.csv gam sendemail "~personal" subject "Your new #domain# account`
|
||||
gam <UserTypeEntity> sendemail recipient|to <RecipientEntity>
|
||||
[replyto <EmailAddress>]
|
||||
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
||||
[subject <String>]
|
||||
[<MessageContent>]
|
||||
[subject <String>] [<MessageContent>]
|
||||
(replace <Tag> <String>)*
|
||||
(replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
|
||||
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
|
||||
(embedimage <FileName> <String>)*
|
||||
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
||||
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
||||
[threadid <String>]
|
||||
```
|
||||
Emails will be sent from the users in `<UserTypeEntity>` to the recipients in `<RecipientEntity>`.
|
||||
|
||||
@@ -395,14 +396,14 @@ Your command line will have: `embedimage file1.jpg image1 embedimage file2.jpg i
|
||||
gam <UserTypeEntity> sendemail from <EmailAddress>
|
||||
[replyto <EmailAddress>]
|
||||
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
||||
[subject <String>]
|
||||
[<MessageContent>]
|
||||
[subject <String>] [<MessageContent>]
|
||||
(replace <Tag> <String>)*
|
||||
(replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
|
||||
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
|
||||
(embedimage <FileName> <String>)*
|
||||
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
||||
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
||||
[threadid <String>]
|
||||
```
|
||||
Emails will be sent to the users in `<UserTypeEntity>`.
|
||||
|
||||
@@ -451,3 +452,29 @@ $ gam csv UserEmail.csv gam user "~User" sendemail to "~To" subject "~Subject" t
|
||||
User: user1@domain.com, Send Email to 1 Recipient
|
||||
Recipient: user2@domain.com, Message: Test, Email Sent: 17677cdfbe1146f4
|
||||
```
|
||||
|
||||
## Conversation mode
|
||||
|
||||
To reply to an email and have Gmail recognize it in conversation mode for the original sender, you have to specify the
|
||||
`References` and `In-Reply-to` headers with the `RFC822 Message ID` from the original message
|
||||
and the `subject` from the original message.
|
||||
```
|
||||
gam user recipient@domain.com sendemail to sender@domain.com references "<CAAMabc...XYZQ@mail.gmail.com>" in-reply-to "<CAAMabc...XYZQ@mail.gmail.com>" subject "Re: Original subject" textmessage "Reply text"
|
||||
```
|
||||
|
||||
If you want to have Gmail recognize the reply in conversation mode in the Sent folder of the original recipient,
|
||||
you must include `threadid <String>`; you can get the 'threadId` with:
|
||||
```
|
||||
gam user recipient@domain.com show messages query "rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>"
|
||||
Getting all Messages that match query ((rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>)) for recipient@domain.com
|
||||
Got 1 Message that matched query ((rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>)) for recipient@domain.com...
|
||||
User: recipient@domain.com, Show 1 Message
|
||||
Message: 19cfd414fe48430d
|
||||
...
|
||||
|
||||
gam user recipient@domain.com sendemail to sender@domain.com references "<CAAMabc...XYZQ@mail.gmail.com>" in-reply-to "<CAAMabc...XYZQ@mail.gmail.com>" subject "Re: Original subject" textmessage "Reply text" threadid 19cfd414fe48430d
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
- [Change Shared Drive visibility](#change-shared-drive-visibility)
|
||||
- [Display Shared Drives](#display-shared-drives)
|
||||
- [Display Shared Drive Counts](#display-shared-drive-counts)
|
||||
- [Display Shared Drive Storage Info](#display-shared-drive-storage-info)
|
||||
- [Display List of Shared Drives in an Organizational Unit](#display-list-of-shared-drives-in-an-organizational-unit)
|
||||
- [Display Count of Shared Drives in an Organizational Unit](#display-count-of-shared-drives-in-an-organizational-unit)
|
||||
- [Display Shared Drive Organizers](#display-shared-drive-organizers)
|
||||
@@ -563,6 +564,21 @@ Windows Command Prompt
|
||||
for /f "delims=" %a in ('gam print shareddrives showitemcountonly') do set count=%a
|
||||
```
|
||||
|
||||
## Display Shared Drive Storage Info
|
||||
|
||||
Get a list of Shared Drives/organizers.
|
||||
```
|
||||
gam redirect csv ./SharedDriveOrganizers.csv print shareddriveorganizers includefileorganizers
|
||||
```
|
||||
Get SharedDrive Drive file count and storage info; use one of the following for size information:
|
||||
* `showsize` - 31549200951 - This is a byte count; include `Size` in `csv_output_header_filter`
|
||||
* `showsizeunits` - 31.55 GB - This is as shown in the Admin console; include `SizeUnits` in csv_output_header_filter
|
||||
```
|
||||
gam config csv_output_header_filter "id,name,Total,Size,SizeUnits,Item cap" csv_input_row_filter "organizers:regex:^.+$"
|
||||
redirect csv ./SharedDriveStorageInfo.csv multiprocess redirect stderr - multiprocess
|
||||
csv ./SharedDriveOrganizers.csv gam user "~organizers" print filecounts select shareddriveid "~id" showsize showsizeunits
|
||||
```
|
||||
|
||||
## Display all Shared Drives with a specific organizer
|
||||
Substitute actual email address for `organizer@domain.com`.
|
||||
```
|
||||
|
||||
@@ -405,7 +405,7 @@ quotaBytesUsed - The number of storage quota bytes used by the file.
|
||||
size - Size in bytes of blobs and first party editor files.
|
||||
```
|
||||
Previously, GAM used the `size` field when totaling file sizes, it now uses the `quotaBytesUsed` field.
|
||||
The option `sizefield quotabytesused|size` allows you to select which field to use.
|
||||
The option `sizefield quotabytesused|size` allows you to select which field to use; `quotabytesused` is the default.
|
||||
|
||||
For most MIME types, the values are the same; for the following MIME types, `quotabytesused` is larger.
|
||||
```
|
||||
@@ -719,7 +719,7 @@ gam <UserTypeEntity> print filecounts [todrive <ToDriveAttribute>*]
|
||||
[filenamematchpattern <REMatchPattern>]
|
||||
<PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
||||
[excludetrashed]
|
||||
[showsize] [showmimetypesize]
|
||||
[showsize] [showsizeunits] [showmimetypesize]
|
||||
[showlastmodification] [pathdelimiter <Character>]
|
||||
(addcsvdata <FieldName> <String>)*
|
||||
[summary none|only|plus] [summaryuser <String>]
|
||||
@@ -735,7 +735,7 @@ gam <UserTypeEntity> show filecounts
|
||||
[filenamematchpattern <REMatchPattern>]
|
||||
<PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
||||
[excludetrashed]
|
||||
[showsize] [showmimetypesize]
|
||||
[showsize] [showsizeunits] [showmimetypesize]
|
||||
[showlastmodification] [pathdelimiter <Character>]
|
||||
[summary none|only|plus] [summaryuser <String>]
|
||||
```
|
||||
@@ -748,7 +748,11 @@ saying that the query is invalid when, in fact, it is but the user does not have
|
||||
When `continueoninvalidquery` is true, GAM prints an error message and proceeds to the next user rather that terminating
|
||||
as it does now. Of course, if the query really is invalid, you will get the message for every user.
|
||||
|
||||
The `showsize` option displays the total size (in bytes) of the files counted.
|
||||
The `showsize` option displays the total size (in bytes) of the files counted; e.g., `31549200951`.
|
||||
With `print filecounts`, this will be in a column labelled `Size`.
|
||||
|
||||
The `showsizeunits` option displays the total size of the files counted with two decimal places and units; e.g., `31.55 GB`.
|
||||
With `print filecounts`, this will be in a column labelled `SizeUnits`.
|
||||
|
||||
The `showmimetypesize` option displays the total size (in bytes) of each MIME type counted.
|
||||
|
||||
@@ -1100,7 +1104,7 @@ gam <UserTypeEntity> print|show filelist [todrive <ToDriveAttribute>*]
|
||||
[excludetrashed]
|
||||
[maxfiles <Integer>] [nodataheaders <String>]
|
||||
[countsonly [summary none|only|plus] [summaryuser <String>]
|
||||
[showsource] [showsize] [showmimetypesize]]
|
||||
[showsource] [showsize] [showsizeunits] [showmimetypesize]]
|
||||
[countsrowfilter]
|
||||
[filepath|fullpath [folderpathonly [<Boolean>]] [pathdelimiter <Character>] [addpathstojson] [showdepth]] [buildtree]
|
||||
[allfields|<DriveFieldName>*|(fields <DriveFieldNameList>)]
|
||||
@@ -1304,7 +1308,9 @@ The `summaryuser <String>` option replaces the default summary user `Summary` w
|
||||
|
||||
The `countsonly` suboption `showsource` adds additional columns `Source` and `Name` that identify the top level folder ID and Name from which the counts are derived.
|
||||
|
||||
The `countsonly` suboption `showsize` adds an additional column `Size` that indicates the total size (in bytes) of the files represented on the row.
|
||||
The `countsonly` suboption `showsize` adds an additional column `Size` that indicates the total size (in bytes) of the files represented on the row; e.g., `31549200951`.
|
||||
|
||||
The `countsonly` suboption `showsizeunits` adds an additional column `SizeUnits` that indicates the total size of the files represented on the row with two decimal places and units; e.g., `31.55 GB`.
|
||||
|
||||
The `countsonly` suboption `showmimetypesize` adds additional columns `<MimeType>:Size` that indicate the total size (in bytes) of each MIME type.
|
||||
|
||||
|
||||
@@ -329,6 +329,20 @@ You can remove all instances of a `<UserMultiAttribute>` with `<UserClearAttribu
|
||||
<UserMultiAttribute>|
|
||||
<UserClearAttribute>
|
||||
```
|
||||
```
|
||||
<UserMultiAttributeFilterName> ::=
|
||||
address|addresses|
|
||||
externalid|externalids|
|
||||
im|ims|
|
||||
keyword|keywords|
|
||||
location|locations|
|
||||
orgainzation|organizations|
|
||||
otheremail|otheremails|
|
||||
phone|phones|
|
||||
relation|relations|
|
||||
website|websites
|
||||
```
|
||||
|
||||
## Admin Console User Info
|
||||
When defining a user in the admin console, there is a section labelled `Employee information` with the following items:
|
||||
* `Employee ID`
|
||||
@@ -971,6 +985,8 @@ gam info user [<UserItem>]
|
||||
[(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
||||
[noschemas|allschemas|(schemas|custom|customschemas <SchemaNameList>)]
|
||||
[userview] <UserFieldName>* [fields <UserFieldNameList>]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[formatjson]
|
||||
```
|
||||
### Display information about multiple users
|
||||
@@ -984,6 +1000,8 @@ gam info users <UserTypeEntity>
|
||||
[(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
||||
[noschemas|allschemas|(schemas|custom|customschemas <SchemaNameList>)]
|
||||
[userview] <UserFieldName>* [fields <UserFieldNameList>]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[formatjson]
|
||||
gam <UserTypeEntity> info users
|
||||
[quick]
|
||||
@@ -994,6 +1012,8 @@ gam <UserTypeEntity> info users
|
||||
[(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
||||
[noschemas|allschemas|(schemas|custom|customschemas <SchemaNameList>)]
|
||||
[userview] <UserFieldName>* [fields <UserFieldNameList>]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[formatjson]
|
||||
```
|
||||
For `info users`, unlike all other GAM commands, a `<UserTypeEntity>` value of `all users` is actually `all users_ns_susp` not `all users_ns`.
|
||||
@@ -1031,6 +1051,11 @@ By default, Gam displays fields that only an adminstrator can view.
|
||||
By default, Gam displays all fields for a user.
|
||||
* `<UserFieldName>* [fields <UserFieldNameList>]` - Only display selected fields.
|
||||
|
||||
By default, all instances of `<UserMultiAttribute>` are displayed, use these options to only display instances
|
||||
of a specified `type` or `customType`.
|
||||
* `filtermultiattrtype <UserMultiAttributeFilterName> <String>` - Display `<UserMultiAttributeFilterName>` if its `type` is `<String>`
|
||||
* `filtermultiattrcustom <UserMultiAttributeFilterName> <String>` - Display `<UserMultiAttributeFilterName>` if its `customType` is `<String>`
|
||||
|
||||
By default, Gam displays the information as an indented list of keys and values.
|
||||
* `formatjson` - Display the fields in JSON format.
|
||||
|
||||
@@ -1062,6 +1087,8 @@ gam print users [todrive <ToDriveAttribute>*]
|
||||
[schemas|custom|customschemas all|<SchemaNameList>]
|
||||
[emailpart|emailparts|username]
|
||||
[userview] [allfields|basic|full|(<UserFieldName>*|fields <UserFieldNameList>)]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[delimiter <Character>] [sortheaders [<Boolean>]] [scalarsfirst [<Boolean>]]
|
||||
[formatjson [quotechar <Character>]] [quoteplusphonenumbers]
|
||||
[issuspended <Boolean>] [isarchived <Boolean>] [aliasmatchpattern <REMatchPattern>]
|
||||
@@ -1088,6 +1115,8 @@ gam print users [todrive <ToDriveAttribute>*] select <UserTypeEntity>
|
||||
[schemas|custom|customschemas all|<SchemaNameList>]
|
||||
[emailpart|emailparts|username]
|
||||
[userview] [basic|full|allfields|(<UserFieldName>*|fields <UserFieldNameList>)]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[delimiter <Character>] [sortheaders [<Boolean>]] [scalarsfirst [<Boolean>]]
|
||||
[formatjson [quotechar <Character>]] [quoteplusphonenumbers]
|
||||
[issuspended <Boolean>] [isarchived <Boolean>] [aliasmatchpattern <REMatchPattern>]
|
||||
@@ -1102,6 +1131,8 @@ gam <UserTypeEntity> print users [todrive <ToDriveAttribute>*]
|
||||
[schemas|custom|customschemas all|<SchemaNameList>]
|
||||
[emailpart|emailparts|username]
|
||||
[userview] [basic|full|allfields|(<UserFieldName>*|fields <UserFieldNameList>)]
|
||||
(filtermultiattrtype <UserMultiAttributeFilterName> <String>)*
|
||||
(filtermultiattrcustom <UserMultiAttributeFilterName> <String>)*
|
||||
[delimiter <Character>] [sortheaders [<Boolean>]] [scalarsfirst [<Boolean>]]
|
||||
[formatjson [quotechar <Character>]] [quoteplusphonenumbers]
|
||||
[issuspended <Boolean>] [isarchived <Boolean>] [aliasmatchpattern <REMatchPattern>]
|
||||
@@ -1137,6 +1168,11 @@ By default, Gam displays only the primary email address for each user.
|
||||
* `schemas|custom all` - Display custom schema information for all schemas.
|
||||
* `schemas|custom <SchemaNameList>` - Display all fields or selected fields of the specified custom schemas
|
||||
|
||||
By default, all instances of `<UserMultiAttribute>` are displayed, use these options to only display instances
|
||||
of a specified `type` or `customType`.
|
||||
* `filtermultiattrtype <UserMultiAttributeFilterName> <String>` - Display `<UserMultiAttributeFilterName>` if its `type` is `<String>`
|
||||
* `filtermultiattrcustom <UserMultiAttributeFilterName> <String>` - Display `<UserMultiAttributeFilterName>` if its `customType` is `<String>`
|
||||
|
||||
By default, when aliases are displayed, all aliases are displayed. Use `aliasmatchpattern <REMatchPattern>`
|
||||
to limit the display of aliases to those that match `<REMatchPattern>`.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ It's important to confirm you are always running an official GAM7 release. The f
|
||||
# GitHub Attestation (Linux/MacOS/Windows)
|
||||
GitHub offers [artifict attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds) which prove if a given GAM binary or archive was built by the [GAM-team/GAM](https://gitHub.com/GAM-team/GAM) project and links to the build job. This offers you certainty that the GAM executable you are running or the GAM package you downloaded were officially generated by the [GAM-team/GAM](https://gitHub.com/GAM-team/GAM) project.
|
||||
|
||||
To verify a given GAM executable file or package (.zip, .msi or .tar.xz) is legitimate, use the following steps:
|
||||
To verify a given GAM executable file or package (.zip, .exe or .tar.xz) is legitimate, use the following steps:
|
||||
1. Install the [GitHub CLI command line tool](https://github.com/cli/cli#installation).
|
||||
2. Login to the tool with the command. You need a [free GitHub account](https://gitHub.com/join) for this.
|
||||
```
|
||||
@@ -27,7 +27,7 @@ gh attestation verify --repo GAM-team/GAM --format=json \
|
||||
|
||||
4. If the GAM file or package is legit you'll see output like:
|
||||
```
|
||||
Loaded digest sha256:a63dc5e71c0b3335865877fc7dc9248bbf7481d22995c18253a2ae71fcb9793a for file://gam-7.00.00-windows-x86_64.msi
|
||||
Loaded digest sha256:a63dc5e71c0b3335865877fc7dc9248bbf7481d22995c18253a2ae71fcb9793a for file://gam-7.00.00-windows-x86_64.exe
|
||||
Loaded 1 attestation from GitHub API
|
||||
✓ Verification succeeded!
|
||||
|
||||
@@ -77,7 +77,7 @@ origin=Developer ID Application: Jay Lee (GZ85H2DRLM)
|
||||
If you do not see "accepted" and "Jay Lee" as the developer ID, there may be a problem. Please report any suspicious files or concerns to the [GAM Group](https://groups.google.com/g/google-apps-manager) or the [GAM Chat Space](https://git.io/gam-chat).
|
||||
|
||||
# Windows Code Sign
|
||||
On Windows, Official gam.exe files and MSI installer packages are signed by a [Certum Open Source code signing certificate](https://shop.certum.eu/open-source-code-signing.html). You can validate the signature and thus be sure you are running official GAM7 from the command line and GUI:
|
||||
On Windows, Official gam.exe files and EXE installer packages are signed by a [Certum Open Source code signing certificate](https://shop.certum.eu/open-source-code-signing.html). You can validate the signature and thus be sure you are running official GAM7 from the command line and GUI:
|
||||
|
||||
# Command Line
|
||||
From PowerShell, run the following command:
|
||||
@@ -113,6 +113,6 @@ SignerCertificate : [Subject]
|
||||
confirm that status is "Valid" and the SignerCertificate says "Open Source Developer, James Lee" (yes, James is Jay's legal name, now you know).
|
||||
|
||||
## GUI
|
||||
From File Manager, you can right click on gam.exe or the MSI package and go to the Digital Signatures tab. From there you'll see the signing certificate which should show "Open Source Developer, James Lee".
|
||||
From File Manager, you can right click on gam.exe or the EXE installer package and go to the Digital Signatures tab. From there you'll see the signing certificate which should show "Open Source Developer, James Lee".
|
||||
|
||||

|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
\# Version and Help
|
||||
# Version and Help
|
||||
|
||||
Print the current version of Gam with details
|
||||
```
|
||||
gam version
|
||||
GAM 7.34.12 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM 7.36.02 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.3 64-bit final
|
||||
macOS Tahoe 26.3 arm64
|
||||
macOS Tahoe 26.3.1 arm64
|
||||
Path: /Users/gamteam/bin/gam7
|
||||
Config File: /Users/gamteam/GamConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
|
||||
Time: 2026-02-15T07:51:00-08:00
|
||||
@@ -15,10 +15,10 @@ Time: 2026-02-15T07:51:00-08:00
|
||||
Print the current version of Gam with details and time offset information
|
||||
```
|
||||
gam version timeoffset
|
||||
GAM 7.34.12 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM 7.36.02 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.3 64-bit final
|
||||
macOS Tahoe 26.3 arm64
|
||||
macOS Tahoe 26.3.1 arm64
|
||||
Path: /Users/gamteam/bin/gam7
|
||||
Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
|
||||
Your system time differs from www.googleapis.com by less than 1 second
|
||||
@@ -27,10 +27,10 @@ Your system time differs from www.googleapis.com by less than 1 second
|
||||
Print the current version of Gam with extended details and SSL information
|
||||
```
|
||||
gam version extended
|
||||
GAM 7.34.12 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM 7.36.02 - https://github.com/GAM-team/GAM - pyinstaller
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.3 64-bit final
|
||||
macOS Tahoe 26.3 arm64
|
||||
macOS Tahoe 26.3.1 arm64
|
||||
Path: /Users/gamteam/bin/gam7
|
||||
Config File: /Users/gamteam/GamConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
|
||||
Time: 2026-02-15T07:51:00-08:00
|
||||
@@ -68,7 +68,7 @@ MacOS High Sierra 10.13.6 x86_64
|
||||
Path: /Users/gamteam/bin/gam7
|
||||
Version Check:
|
||||
Current: 5.35.08
|
||||
Latest: 7.34.12
|
||||
Latest: 7.36.02
|
||||
echo $?
|
||||
1
|
||||
```
|
||||
@@ -76,7 +76,7 @@ echo $?
|
||||
Print the current version number without details
|
||||
```
|
||||
gam version simple
|
||||
7.34.12
|
||||
7.36.02
|
||||
```
|
||||
In Linux/MacOS you can do:
|
||||
```
|
||||
@@ -86,10 +86,10 @@ echo $VER
|
||||
Print the current version of Gam and address of this Wiki
|
||||
```
|
||||
gam help
|
||||
GAM 7.34.12 - https://github.com/GAM-team/GAM
|
||||
GAM 7.36.02 - https://github.com/GAM-team/GAM
|
||||
GAM Team <google-apps-manager@googlegroups.com>
|
||||
Python 3.14.3 64-bit final
|
||||
macOS Tahoe 26.3 arm64
|
||||
macOS Tahoe 26.3.1 arm64
|
||||
Path: /Users/gamteam/bin/gam7
|
||||
Config File: /Users/gamteam/GamConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
|
||||
Time: 2026-02-15T07:51:00-08:00
|
||||
|
||||
Reference in New Issue
Block a user