160 lines
5.2 KiB
C#
160 lines
5.2 KiB
C#
namespace MxGateway.Server.Security.Authentication;
|
|
|
|
public static class ApiKeyAdminCommandLineParser
|
|
{
|
|
public static ApiKeyAdminParseResult Parse(IReadOnlyList<string> args)
|
|
{
|
|
if (args.Count == 0 || !string.Equals(args[0], "apikey", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ApiKeyAdminParseResult.NotApiKeyCommand();
|
|
}
|
|
|
|
if (args.Count < 2)
|
|
{
|
|
return ApiKeyAdminParseResult.Fail("Missing apikey subcommand.");
|
|
}
|
|
|
|
if (!TryParseKind(args[1], out ApiKeyAdminCommandKind kind))
|
|
{
|
|
return ApiKeyAdminParseResult.Fail($"Unknown apikey subcommand '{args[1]}'.");
|
|
}
|
|
|
|
Dictionary<string, string?> options = new(StringComparer.OrdinalIgnoreCase);
|
|
bool json = false;
|
|
|
|
for (int index = 2; index < args.Count; index++)
|
|
{
|
|
string arg = args[index];
|
|
if (string.Equals(arg, "--json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
json = true;
|
|
continue;
|
|
}
|
|
|
|
if (!arg.StartsWith("--", StringComparison.Ordinal))
|
|
{
|
|
return ApiKeyAdminParseResult.Fail($"Unexpected argument '{arg}'.");
|
|
}
|
|
|
|
string name = arg[2..];
|
|
string? value;
|
|
|
|
int equalsIndex = name.IndexOf('=', StringComparison.Ordinal);
|
|
if (equalsIndex >= 0)
|
|
{
|
|
value = name[(equalsIndex + 1)..];
|
|
name = name[..equalsIndex];
|
|
}
|
|
else
|
|
{
|
|
if (index + 1 >= args.Count || args[index + 1].StartsWith("--", StringComparison.Ordinal))
|
|
{
|
|
return ApiKeyAdminParseResult.Fail($"Option '--{name}' requires a value.");
|
|
}
|
|
|
|
value = args[++index];
|
|
}
|
|
|
|
options[name] = value;
|
|
}
|
|
|
|
string? keyId = GetOption(options, "key-id");
|
|
string? displayName = GetOption(options, "display-name");
|
|
IReadOnlySet<string> scopes = ParseScopes(GetOption(options, "scopes"));
|
|
|
|
string? validationError = Validate(kind, keyId, displayName);
|
|
if (validationError is not null)
|
|
{
|
|
return ApiKeyAdminParseResult.Fail(validationError);
|
|
}
|
|
|
|
return ApiKeyAdminParseResult.Success(new ApiKeyAdminCommand(
|
|
Kind: kind,
|
|
Json: json,
|
|
SqlitePath: GetOption(options, "sqlite-path"),
|
|
Pepper: GetOption(options, "pepper"),
|
|
KeyId: keyId,
|
|
DisplayName: displayName,
|
|
Scopes: scopes));
|
|
}
|
|
|
|
private static bool TryParseKind(string value, out ApiKeyAdminCommandKind kind)
|
|
{
|
|
switch (value.ToLowerInvariant())
|
|
{
|
|
case "init-db":
|
|
kind = ApiKeyAdminCommandKind.InitDb;
|
|
return true;
|
|
case "create-key":
|
|
kind = ApiKeyAdminCommandKind.CreateKey;
|
|
return true;
|
|
case "list-keys":
|
|
kind = ApiKeyAdminCommandKind.ListKeys;
|
|
return true;
|
|
case "revoke-key":
|
|
kind = ApiKeyAdminCommandKind.RevokeKey;
|
|
return true;
|
|
case "rotate-key":
|
|
kind = ApiKeyAdminCommandKind.RotateKey;
|
|
return true;
|
|
default:
|
|
kind = default;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static string? Validate(ApiKeyAdminCommandKind kind, string? keyId, string? displayName)
|
|
{
|
|
if (kind is ApiKeyAdminCommandKind.CreateKey or ApiKeyAdminCommandKind.RevokeKey or ApiKeyAdminCommandKind.RotateKey
|
|
&& string.IsNullOrWhiteSpace(keyId))
|
|
{
|
|
return $"Subcommand '{KindName(kind)}' requires --key-id.";
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(keyId) && !IsValidKeyId(keyId))
|
|
{
|
|
return "API key id may contain only letters, numbers, periods, and hyphens.";
|
|
}
|
|
|
|
if (kind == ApiKeyAdminCommandKind.CreateKey && string.IsNullOrWhiteSpace(displayName))
|
|
{
|
|
return "Subcommand 'create-key' requires --display-name.";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string KindName(ApiKeyAdminCommandKind kind)
|
|
{
|
|
return kind switch
|
|
{
|
|
ApiKeyAdminCommandKind.InitDb => "init-db",
|
|
ApiKeyAdminCommandKind.CreateKey => "create-key",
|
|
ApiKeyAdminCommandKind.ListKeys => "list-keys",
|
|
ApiKeyAdminCommandKind.RevokeKey => "revoke-key",
|
|
ApiKeyAdminCommandKind.RotateKey => "rotate-key",
|
|
_ => kind.ToString()
|
|
};
|
|
}
|
|
|
|
private static bool IsValidKeyId(string keyId)
|
|
{
|
|
return keyId.All(character =>
|
|
char.IsAsciiLetterOrDigit(character)
|
|
|| character is '.' or '-');
|
|
}
|
|
|
|
private static string? GetOption(Dictionary<string, string?> options, string name)
|
|
{
|
|
return options.TryGetValue(name, out string? value) ? value : null;
|
|
}
|
|
|
|
private static IReadOnlySet<string> ParseScopes(string? scopes)
|
|
{
|
|
return new HashSet<string>(
|
|
(scopes ?? string.Empty)
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
|
|
StringComparer.Ordinal);
|
|
}
|
|
}
|