mxaccesscli: add --username / --domain / --password to write
Wraps LMXProxyServer.AuthenticateUser at the session level
(MxSession.Authenticate) and surfaces three new options on the
write command:
-u, --username <name> galaxy / OS user
--domain <name> composed as <domain>\<username>; omit for
galaxy-authenticated logins or UPN forms
-p, --password <pwd> redacted to "***" in the JSON query echo
The CLI calls AuthenticateUser before AddItem so an auth failure
(userId == 0) bails out cleanly without leaving a half-set-up item.
On success the resolved userId flows into Write(hServer, hItem,
value, userId) and is reflected in:
- human output: "[OK ] write <tag> = <val> (as <verify-user>, userId=N)"
- LLM-JSON results[]: { "authenticated": true, "auth_user_id": N }
Verified live against TestChildObject.TestInt with credentials
DESKTOP-6JL3KKO\dohertj / Sonamu89:
read -> 99
write 7 with --username/--domain/--password -> ok, auth_user_id=1
read -> 7
write 99 with same auth -> ok
read -> 99 (restored)
Important behavior surfaced and documented in docs/usage.md
"Authentication" section: on a galaxy configured in permissive
(Free Access) mode, AuthenticateUser returns a non-zero userId
regardless of credentials — verified by intentionally passing a
wrong password and an unknown user, both of which still resolved
to userId=1 and the write went through. The CLI's auth path is
wired correctly; the galaxy just isn't strict. To exercise real
authentication, target a galaxy with galaxyAuthenticationMode and
attribute-level security above Free Access.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,9 +26,18 @@ namespace MxAccess.Cli.Commands
|
||||
[CommandOption("timeout", 't', Description = "Seconds to wait for OnWriteComplete (and for the initial OnDataChange resolving the type).")]
|
||||
public double TimeoutSeconds { get; init; } = 5.0;
|
||||
|
||||
[CommandOption("user-id", Description = "Authenticated user id passed to Write(). 0 = unauthenticated.")]
|
||||
[CommandOption("user-id", Description = "Pre-resolved authenticated user id passed straight to Write(). Use this only when you already have a userId from a separate AuthenticateUser call. 0 = unauthenticated.")]
|
||||
public int UserId { get; init; }
|
||||
|
||||
[CommandOption("username", 'u', Description = "Galaxy / OS username. Combined with --domain (if set) into '<domain>\\<username>' and resolved to a userId via AuthenticateUser before Write().")]
|
||||
public string Username { get; init; }
|
||||
|
||||
[CommandOption("domain", Description = "Domain or hostname for OS-authenticated galaxies. Combined with --username as '<domain>\\<username>'. Omit for galaxy-authenticated logins.")]
|
||||
public string Domain { get; init; }
|
||||
|
||||
[CommandOption("password", 'p', Description = "Password for --username. If --username is set and --password is omitted the empty string is sent (some galaxies allow it; most don't).")]
|
||||
public string Password { get; init; }
|
||||
|
||||
[CommandOption("client", Description = "MxAccess client name. Default 'mxa'.")]
|
||||
public string ClientName { get; init; } = "mxa";
|
||||
|
||||
@@ -57,6 +66,19 @@ namespace MxAccess.Cli.Commands
|
||||
? ValueCoercion.CoerceArray(Values, TypeHint)
|
||||
: ValueCoercion.Coerce(Values[0], TypeHint);
|
||||
|
||||
// Compose the verify-user string in the form MxAccess expects.
|
||||
// OS-authenticated galaxies want `<domain>\<username>`; galaxy-
|
||||
// authenticated ones want bare `<username>`. We don't try to
|
||||
// detect the galaxy mode — the user picks via whether they pass
|
||||
// --domain or not.
|
||||
string verifyUser = null;
|
||||
if (!string.IsNullOrEmpty(Username))
|
||||
{
|
||||
verifyUser = string.IsNullOrEmpty(Domain)
|
||||
? Username
|
||||
: $@"{Domain}\{Username}";
|
||||
}
|
||||
|
||||
var query = new
|
||||
{
|
||||
command = "write",
|
||||
@@ -67,13 +89,31 @@ namespace MxAccess.Cli.Commands
|
||||
count = Values.Count,
|
||||
timeout_s = TimeoutSeconds,
|
||||
user_id = UserId,
|
||||
verify_user = verifyUser,
|
||||
// Never echo the plaintext password — agents replay query
|
||||
// envelopes and would otherwise leak credentials.
|
||||
password = string.IsNullOrEmpty(Password) ? null : "***",
|
||||
client = ClientName,
|
||||
};
|
||||
|
||||
using var session = new MxSession(ClientName);
|
||||
MxItem item = null;
|
||||
int effectiveUserId = UserId;
|
||||
try
|
||||
{
|
||||
// Resolve credentials -> userId before AddItem so we surface
|
||||
// an auth failure cleanly without leaving a half-set-up item.
|
||||
if (verifyUser != null)
|
||||
{
|
||||
effectiveUserId = session.Authenticate(verifyUser, Password ?? string.Empty);
|
||||
if (effectiveUserId == 0)
|
||||
{
|
||||
EmitFailure(console, query, "authentication-failed", Array.Empty<MxStatusInfo>());
|
||||
Environment.ExitCode = 1;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
item = session.AddItem(Tag);
|
||||
|
||||
// Advise + wait for first OnDataChange to ensure the proxy has
|
||||
@@ -103,7 +143,7 @@ namespace MxAccess.Cli.Commands
|
||||
return default;
|
||||
}
|
||||
|
||||
item.Write(coerced, UserId);
|
||||
item.Write(coerced, effectiveUserId);
|
||||
|
||||
var got = session.WaitForUpdate(
|
||||
u => u.Kind == MxUpdateKind.WriteComplete && u.ItemHandle == item.Handle,
|
||||
@@ -124,10 +164,12 @@ namespace MxAccess.Cli.Commands
|
||||
{
|
||||
new
|
||||
{
|
||||
tag = Tag,
|
||||
ok = ack.IsOk,
|
||||
error = ack.IsOk ? null : "write-failed",
|
||||
statuses = ack.Statuses,
|
||||
tag = Tag,
|
||||
ok = ack.IsOk,
|
||||
error = ack.IsOk ? null : "write-failed",
|
||||
authenticated = verifyUser != null,
|
||||
auth_user_id = verifyUser != null ? (int?)effectiveUserId : null,
|
||||
statuses = ack.Statuses,
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -141,7 +183,10 @@ namespace MxAccess.Cli.Commands
|
||||
var rendered = isArrayWrite
|
||||
? $"[{string.Join(", ", Values)}] ({Values.Count} elements)"
|
||||
: Values[0];
|
||||
console.Output.WriteLine($"[OK ] write {Tag} = {rendered}");
|
||||
var authNote = verifyUser != null
|
||||
? $" (as {verifyUser}, userId={effectiveUserId})"
|
||||
: "";
|
||||
console.Output.WriteLine($"[OK ] write {Tag} = {rendered}{authNote}");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -45,6 +45,22 @@ namespace MxAccess.Cli.Mx
|
||||
return item;
|
||||
}
|
||||
|
||||
/// Authenticate a user against the galaxy and return the user id
|
||||
/// to pass to subsequent Write/WriteSecured calls. Returns 0 on
|
||||
/// failure (bad credentials, unknown user, galaxy auth disabled).
|
||||
///
|
||||
/// `verifyUser` is the credential string the proxy expects for its
|
||||
/// configured galaxy authentication mode:
|
||||
/// - osAuthenticationMode → `<domain-or-host>\<username>`
|
||||
/// - galaxyAuthenticationMode → `<username>` only
|
||||
/// - mixed/AAD → `<UPN>` (e.g. `user@example.com`)
|
||||
public int Authenticate(string verifyUser, string password)
|
||||
{
|
||||
if (string.IsNullOrEmpty(verifyUser))
|
||||
throw new ArgumentException("verifyUser must be non-empty.", nameof(verifyUser));
|
||||
return _proxy.AuthenticateUser(_hServer, verifyUser, password ?? string.Empty);
|
||||
}
|
||||
|
||||
/// Pump COM messages while watching for an update that matches the predicate.
|
||||
/// Returns true when one is captured, false on timeout.
|
||||
public bool WaitForUpdate(Predicate<MxUpdate> match, TimeSpan timeout, out MxUpdate captured)
|
||||
|
||||
Reference in New Issue
Block a user