mirror of
https://github.com/dmcxblue/ANIMO
synced 2026-08-04 16:10:26 +00:00
Login: forward operator-chosen resource through Az PS + az cli
The three login scripts were minting only management.azure.com tokens
regardless of the resource the operator picked in the UI dropdown -
because none of the pieces in the chain honoured it. az cli was
similarly minting only its default ARM token. Now the operator's
choice reaches both toolchains end-to-end.
- server/scripts/login_azure.ps1
- server/scripts/login_azure_devicecode.ps1
- server/scripts/login_spn_azure.ps1
Add `[string]$Resource='https://management.azure.com'` param.
Get-AzAccessToken now runs with -ResourceUrl $Resource (previously
hardcoded to https://management.azure.com or omitted, defaulting
to ARM). After `az login` succeeds, also fetch a resource-specific
token via `az account get-access-token --resource $Resource -o tsv`
so both Az PS and az cli have a warm token cache for the chosen
audience. Device-code hint now includes the exact
`az account get-access-token --resource ...` line.
- server/Server.cpp
handleNewSession reads req["resource"] (default ARM) into a local
and forwards it as `-Resource '<escaped>'` on all three script
invocations (credential/ROPC, device-code, SPN). Uses the existing
escapePsString helper. Also stops the SPN meta-write from
hardcoding management.azure.com - it now records the actual
resource. The old ternary that always returned ARM is replaced
with a plain default.
- client/CredentialLoginWindow.{h,cpp}
Dropdown expanded from 2 to 6 entries (Management, Graph, Key
Vault, Storage, SQL, Other...). currentData() carries the resource
URL - same pattern SPNLoginWindow already uses. "Other..." reveals
a free-text QLineEdit; a trailing "/.default" scope suffix is
stripped so -ResourceUrl gets a v1 audience URL.
- client/DeviceCodeLoginWindow.cpp
startFullServerSession() no longer hardcodes ARM - reads
resourceInput->text() (the free-text field that was already in the
UI but silently ignored). Empty falls back to ARM; a trailing
/.default is stripped for the same reason.
- client/SPNLoginWindow.cpp
authenticateViaPowerShell() no longer hardcodes ARM in req -
forwards pendingResource (already populated from the dropdown at
handler entry). Empty falls back to ARM defensively.
This commit is contained in:
@@ -31,10 +31,30 @@ void CredentialLoginWindow::setupUi() {
|
||||
layout->addWidget(new QLabel("Select resource and enter credentials"));
|
||||
|
||||
resourceDropdown = new QComboBox(this);
|
||||
resourceDropdown->addItem("Azure Management (management.azure.com)");
|
||||
resourceDropdown->addItem("Microsoft Graph (graph.microsoft.com)");
|
||||
resourceDropdown->addItem("Azure Management (management.azure.com)",
|
||||
QStringLiteral("https://management.azure.com"));
|
||||
resourceDropdown->addItem("Microsoft Graph (graph.microsoft.com)",
|
||||
QStringLiteral("https://graph.microsoft.com"));
|
||||
resourceDropdown->addItem("Key Vault (vault.azure.net)",
|
||||
QStringLiteral("https://vault.azure.net"));
|
||||
resourceDropdown->addItem("Azure Storage (storage.azure.com)",
|
||||
QStringLiteral("https://storage.azure.com"));
|
||||
resourceDropdown->addItem("SQL Database (database.windows.net)",
|
||||
QStringLiteral("https://database.windows.net"));
|
||||
resourceDropdown->addItem("Other...", QStringLiteral(""));
|
||||
layout->addWidget(resourceDropdown);
|
||||
|
||||
// Free-text field shown only when the operator picks "Other..."
|
||||
customResource = new QLineEdit(this);
|
||||
customResource->setPlaceholderText("Custom resource URL, e.g. https://vault.azure.net");
|
||||
customResource->setVisible(false);
|
||||
layout->addWidget(customResource);
|
||||
connect(resourceDropdown, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, [this](int) {
|
||||
const bool isOther = resourceDropdown->currentData().toString().isEmpty();
|
||||
customResource->setVisible(isOther);
|
||||
});
|
||||
|
||||
username = new QLineEdit(this);
|
||||
username->setPlaceholderText("e.g. alice@contoso.com");
|
||||
layout->addWidget(username);
|
||||
@@ -97,10 +117,17 @@ void CredentialLoginWindow::handleLogin() {
|
||||
|
||||
sessionHandled = false;
|
||||
|
||||
if (resourceDropdown->currentIndex() == 1)
|
||||
pendingResource = QStringLiteral("https://graph.microsoft.com");
|
||||
else
|
||||
pendingResource = QStringLiteral("https://management.azure.com");
|
||||
pendingResource = resourceDropdown->currentData().toString();
|
||||
if (pendingResource.isEmpty()) {
|
||||
// "Other..." selected - use the free-text field. Empty falls back to ARM.
|
||||
pendingResource = customResource->text().trimmed();
|
||||
if (pendingResource.isEmpty())
|
||||
pendingResource = QStringLiteral("https://management.azure.com");
|
||||
// Strip a trailing "/.default" scope suffix so -ResourceUrl gets a
|
||||
// v1 audience URL.
|
||||
if (pendingResource.endsWith(QStringLiteral("/.default")))
|
||||
pendingResource.chop(QStringLiteral("/.default").size());
|
||||
}
|
||||
|
||||
pendingUsername = user;
|
||||
pendingRid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
@@ -49,6 +49,7 @@ private:
|
||||
private:
|
||||
DashboardWindow *parentDashboard = nullptr;
|
||||
QComboBox *resourceDropdown = nullptr;
|
||||
QLineEdit *customResource = nullptr; // Shown only when "Other..." is picked
|
||||
QLineEdit *username = nullptr;
|
||||
QLineEdit *password = nullptr;
|
||||
|
||||
|
||||
@@ -197,7 +197,16 @@ void DeviceCodeLoginWindow::startFullServerSession() {
|
||||
fullSessionMode = true;
|
||||
fullSessionSid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
pendingRid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
pendingResource = QStringLiteral("https://management.azure.com");
|
||||
|
||||
// Honour the operator-typed resource. The field default is the v2 SCOPE form
|
||||
// (".../.default"); Az PS's -ResourceUrl wants the v1 audience without the
|
||||
// suffix, so strip it. Empty -> ARM default.
|
||||
pendingResource = resourceInput->text().trimmed();
|
||||
if (pendingResource.isEmpty()) {
|
||||
pendingResource = QStringLiteral("https://management.azure.com");
|
||||
} else if (pendingResource.endsWith(QStringLiteral("/.default"))) {
|
||||
pendingResource.chop(QStringLiteral("/.default").size());
|
||||
}
|
||||
|
||||
createStatusWidget(fullSessionSid);
|
||||
if (statusWidgets.contains(fullSessionSid))
|
||||
|
||||
@@ -391,10 +391,17 @@ void SPNLoginWindow::authenticateViaPowerShell(const QString &appId,
|
||||
|
||||
pendingRid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
// Use whatever resource the operator picked in the dropdown - not a
|
||||
// hardcoded ARM. pendingResource is populated when the user changes the
|
||||
// dropdown; if it's empty (window opened but never touched), default to
|
||||
// ARM.
|
||||
QString resource = pendingResource;
|
||||
if (resource.isEmpty()) resource = QStringLiteral("https://management.azure.com");
|
||||
|
||||
QJsonObject req;
|
||||
req.insert(Protocol::F_ACTION, Protocol::ACTION_NEW_SESSION);
|
||||
req.insert("mode", QStringLiteral("spn"));
|
||||
req.insert("resource", QStringLiteral("https://management.azure.com"));
|
||||
req.insert("resource", resource);
|
||||
req.insert("appId", appId);
|
||||
req.insert("clientSecret", secret);
|
||||
req.insert("tenantId", tenantId);
|
||||
|
||||
+9
-8
@@ -862,8 +862,8 @@ bool Server::handleLine(QTcpSocket *sock, const QByteArray &line) {
|
||||
// ── Create session (credentials or raw) ────────────────────────────────────
|
||||
if (action == Protocol::ACTION_NEW_SESSION) {
|
||||
const QString mode = obj.value("mode").toString();
|
||||
const QString resource = obj.value("resource").toString(APP_CONFIG.defaultClientId().isEmpty()
|
||||
? QStringLiteral("https://management.azure.com") : QStringLiteral("https://management.azure.com"));
|
||||
QString resource = obj.value("resource").toString().trimmed();
|
||||
if (resource.isEmpty()) resource = QStringLiteral("https://management.azure.com");
|
||||
QString sid = obj.value("sessionId").toString().trimmed();
|
||||
if (sid.isEmpty()) sid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
const QString rid = obj.value("rid").toString();
|
||||
@@ -1000,8 +1000,8 @@ bool Server::handleLine(QTcpSocket *sock, const QByteArray &line) {
|
||||
}
|
||||
|
||||
// Use single quotes with proper escaping to prevent command injection
|
||||
QString execCmd = QString(". \"%1\" -Username '%2' -Password '%3'\n")
|
||||
.arg(ps1Path, escapePsString(user), escapePsString(pass));
|
||||
QString execCmd = QString(". \"%1\" -Username '%2' -Password '%3' -Resource '%4'\n")
|
||||
.arg(ps1Path, escapePsString(user), escapePsString(pass), escapePsString(resource));
|
||||
proc->write(execCmd.toUtf8());
|
||||
|
||||
QJsonObject ack = Protocol::ok("new_session ok");
|
||||
@@ -1039,7 +1039,8 @@ bool Server::handleLine(QTcpSocket *sock, const QByteArray &line) {
|
||||
}
|
||||
|
||||
// No credentials to pass - the device-code prompt streams to the Session Tab.
|
||||
QString execCmd = QString(". \"%1\"\n").arg(ps1Path);
|
||||
QString execCmd = QString(". \"%1\" -Resource '%2'\n")
|
||||
.arg(ps1Path, escapePsString(resource));
|
||||
proc->write(execCmd.toUtf8());
|
||||
|
||||
QJsonObject ack = Protocol::ok("new_session ok");
|
||||
@@ -1107,15 +1108,15 @@ bool Server::handleLine(QTcpSocket *sock, const QByteArray &line) {
|
||||
spnMeta.insert("user", appId);
|
||||
spnMeta.insert("tenantId", tenantId);
|
||||
spnMeta.insert("domain", QStringLiteral("ServicePrincipal"));
|
||||
spnMeta.insert("resource", QStringLiteral("https://management.azure.com"));
|
||||
spnMeta.insert("resource", resource);
|
||||
g_sessionInfo.insert(sid, spnMeta);
|
||||
}
|
||||
SessionDBManager::instance().updateSessionUser(sid, appId);
|
||||
SessionDBManager::instance().updateSessionTenant(sid, tenantId, QStringLiteral("ServicePrincipal"));
|
||||
|
||||
// Pass only the credential file path, not the secret itself
|
||||
QString execCmd = QString(". \"%1\" -CredentialFile \"%2\"\n")
|
||||
.arg(ps1Path, credPath);
|
||||
QString execCmd = QString(". \"%1\" -CredentialFile \"%2\" -Resource '%3'\n")
|
||||
.arg(ps1Path, credPath, escapePsString(resource));
|
||||
proc->write(execCmd.toUtf8());
|
||||
|
||||
// The script deletes this after reading, but wipe it here too in case
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
param([string]$Username,[string]$Password)
|
||||
param([string]$Username,[string]$Password,[string]$Resource='https://management.azure.com')
|
||||
$ErrorActionPreference='Stop'
|
||||
$mfa='AADSTS50076|AADSTS50079|AADSTS50158|AADSTS53003|AADSTS50074|AADSTS500121'
|
||||
try {
|
||||
@@ -11,7 +11,7 @@ try {
|
||||
Connect-AzAccount -Credential $c -EA Stop -WA Ignore|Out-Null
|
||||
$ctx=Get-AzContext -EA SilentlyContinue
|
||||
if($ctx-and$ctx.Account){
|
||||
try{$t=(Get-AzAccessToken -ResourceUrl 'https://management.azure.com' -EA Stop).Token;Write-Output "__ANIMO_TOKEN__:$t"}catch{}
|
||||
try{$t=(Get-AzAccessToken -ResourceUrl $Resource -EA Stop).Token;Write-Output "__ANIMO_TOKEN__:$t"}catch{}
|
||||
# Also log az cli in with the same ROPC creds so operators can use az one-liners
|
||||
# in the same terminal (az cli keeps a separate token cache from Az PS).
|
||||
try {
|
||||
@@ -22,6 +22,15 @@ try {
|
||||
$azOut = (& az @azArgs) 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Output "[Animo] az cli logged in (both Az PS and az cli contexts active)"
|
||||
# Also mint a resource-specific token via az cli so both toolchains
|
||||
# have a matching token for the chosen resource. Silent on failure -
|
||||
# the Az PS __ANIMO_TOKEN__ above is what the server captures.
|
||||
try {
|
||||
$azTok = (& az account get-access-token --resource $Resource -o tsv --query accessToken 2>$null)
|
||||
if ($LASTEXITCODE -eq 0 -and $azTok) {
|
||||
Write-Output "[Animo] az cli minted token for $Resource"
|
||||
}
|
||||
} catch {}
|
||||
} else {
|
||||
Write-Output ("[Animo] az cli login failed (exit {0}): {1}" -f $LASTEXITCODE, ($azOut | Out-String).Trim())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
param([string]$Resource='https://management.azure.com')
|
||||
$ErrorActionPreference='Stop'
|
||||
# Full interactive login: device code runs INSIDE Az, so the context keeps a real
|
||||
# token cache + refresh token. Unlike -AccessToken, this lets Get-AzAccessToken mint
|
||||
@@ -12,7 +13,7 @@ try {
|
||||
ForEach-Object { Write-Output $_ }
|
||||
$ctx=Get-AzContext -EA SilentlyContinue
|
||||
if($ctx-and$ctx.Account){
|
||||
try{$t=(Get-AzAccessToken -ResourceUrl 'https://management.azure.com' -EA Stop).Token;Write-Output "__ANIMO_TOKEN__:$t"}catch{}
|
||||
try{$t=(Get-AzAccessToken -ResourceUrl $Resource -EA Stop).Token;Write-Output "__ANIMO_TOKEN__:$t"}catch{}
|
||||
# az cli is a separate binary with its own token cache. For device-code
|
||||
# login we deliberately don't call `az login` here - it would prompt the
|
||||
# operator for a SECOND device code, which is confusing. If they need az
|
||||
@@ -20,6 +21,7 @@ try {
|
||||
try {
|
||||
if (Get-Command az -EA SilentlyContinue) {
|
||||
Write-Output "[Animo] To use az cli in this session run: az login --use-device-code --tenant $($ctx.Tenant.Id)"
|
||||
Write-Output "[Animo] Then: az account get-access-token --resource $Resource"
|
||||
}
|
||||
} catch {}
|
||||
Write-Output "__ANIMO_LOGIN_OK__:$($ctx.Account)"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
param([string]$CredentialFile)
|
||||
param([string]$CredentialFile,[string]$Resource='https://management.azure.com')
|
||||
$ErrorActionPreference='Stop'
|
||||
try {
|
||||
if(-not(Test-Path $CredentialFile)){throw "Credential file not found"}
|
||||
@@ -37,6 +37,15 @@ try {
|
||||
$azOut = (& az login --service-principal -u $AppId -p $ClientSecret --tenant $TenantId --allow-no-subscriptions --output none) 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Output "[Animo] az cli logged in as SPN (both Az PS and az cli contexts active)"
|
||||
# Warm the az cli token cache for the chosen resource so both
|
||||
# toolchains have parity. Silent on failure - Az PS still emitted
|
||||
# __ANIMO_TOKEN__ below.
|
||||
try {
|
||||
$azTok = (& az account get-access-token --resource $Resource -o tsv --query accessToken 2>$null)
|
||||
if ($LASTEXITCODE -eq 0 -and $azTok) {
|
||||
Write-Output "[Animo] az cli minted token for $Resource"
|
||||
}
|
||||
} catch {}
|
||||
} else {
|
||||
Write-Output ("[Animo] az cli login failed (exit {0}): {1}" -f $LASTEXITCODE, ($azOut | Out-String).Trim())
|
||||
}
|
||||
@@ -50,7 +59,7 @@ try {
|
||||
$ctx=Get-AzContext -EA SilentlyContinue
|
||||
if($ctx-and$ctx.Account){
|
||||
try{
|
||||
$tok=Get-AzAccessToken -EA SilentlyContinue
|
||||
$tok=Get-AzAccessToken -ResourceUrl $Resource -EA SilentlyContinue
|
||||
if($tok-and$tok.Token){Write-Output "__ANIMO_TOKEN__:$($tok.Token)"}
|
||||
}catch{}
|
||||
Write-Output "__ANIMO_LOGIN_OK__:$($ctx.Account)"
|
||||
|
||||
Reference in New Issue
Block a user