721 lines
28 KiB
C#
721 lines
28 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using Lskj.CommandKernel;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
namespace Lskj.AgentBridge
|
|
{
|
|
/// <summary>
|
|
/// Narrows the commands already authorized by the ERP. This policy never
|
|
/// grants an ERP permission; it only applies a customer deployment rollout
|
|
/// boundary after the normal ERP permission checks have succeeded.
|
|
/// </summary>
|
|
public sealed class CommandRolloutPolicy : ICommandAuthorizer
|
|
{
|
|
public const int MaximumBytes = 256 * 1024;
|
|
public const int MaximumRules = 128;
|
|
public const int MaximumScopeValues = 64;
|
|
|
|
private const string ConfigEnvironment = "LSERP_AGENT_ROLLOUT_CONFIG";
|
|
private const string Sha256Environment = "LSERP_AGENT_ROLLOUT_SHA256";
|
|
private const string CustomerEnvironment = "LSERP_AGENT_ROLLOUT_CUSTOMER_ID";
|
|
|
|
private static readonly Regex SafeIdentifier = new Regex(
|
|
"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$",
|
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
|
private static readonly Regex SafeSha256 = new Regex(
|
|
"^[A-Fa-f0-9]{64}$",
|
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
|
|
|
private readonly Dictionary<string, CommandRolloutRule> _rules;
|
|
private readonly bool _defaultAllow;
|
|
|
|
private CommandRolloutPolicy(
|
|
bool configured,
|
|
string customerId,
|
|
string databaseScopeFingerprint,
|
|
string sourceSha256,
|
|
bool defaultAllow,
|
|
IEnumerable<CommandRolloutRule> rules)
|
|
{
|
|
Configured = configured;
|
|
CustomerId = customerId;
|
|
DatabaseScopeFingerprint = databaseScopeFingerprint;
|
|
SourceSha256 = sourceSha256;
|
|
_defaultAllow = defaultAllow;
|
|
_rules = new Dictionary<string, CommandRolloutRule>(
|
|
StringComparer.OrdinalIgnoreCase);
|
|
foreach (CommandRolloutRule rule in rules ?? new CommandRolloutRule[0])
|
|
_rules.Add(rule.Command, rule);
|
|
}
|
|
|
|
public bool Configured { get; private set; }
|
|
public string CustomerId { get; private set; }
|
|
public string DatabaseScopeFingerprint { get; private set; }
|
|
public string SourceSha256 { get; private set; }
|
|
public string DefaultAction
|
|
{
|
|
get { return _defaultAllow ? "allow" : "deny"; }
|
|
}
|
|
public int RuleCount
|
|
{
|
|
get { return _rules.Count; }
|
|
}
|
|
|
|
public static CommandRolloutPolicy AllowAll()
|
|
{
|
|
return new CommandRolloutPolicy(
|
|
false,
|
|
null,
|
|
null,
|
|
null,
|
|
true,
|
|
new CommandRolloutRule[0]);
|
|
}
|
|
|
|
public static CommandRolloutPolicy FromEnvironment()
|
|
{
|
|
return FromEnvironment(false);
|
|
}
|
|
|
|
public static CommandRolloutPolicy FromEnvironment(
|
|
bool requireFailClosedConfiguration)
|
|
{
|
|
string path = Environment.GetEnvironmentVariable(ConfigEnvironment);
|
|
string expectedSha256 =
|
|
Environment.GetEnvironmentVariable(Sha256Environment);
|
|
string expectedCustomerId =
|
|
Environment.GetEnvironmentVariable(CustomerEnvironment);
|
|
|
|
if (path == null && expectedSha256 == null && expectedCustomerId == null)
|
|
{
|
|
if (requireFailClosedConfiguration)
|
|
throw Invalid("商用命令桥必须配置默认拒绝的命令发布策略。");
|
|
return AllowAll();
|
|
}
|
|
if (string.IsNullOrWhiteSpace(path)
|
|
|| string.IsNullOrWhiteSpace(expectedSha256)
|
|
|| string.IsNullOrWhiteSpace(expectedCustomerId))
|
|
{
|
|
throw Invalid(
|
|
"命令发布配置路径、SHA-256 和客户部署标识必须同时设置。");
|
|
}
|
|
CommandRolloutPolicy result = Load(
|
|
path,
|
|
expectedSha256,
|
|
expectedCustomerId);
|
|
if (requireFailClosedConfiguration && result._defaultAllow)
|
|
throw Invalid("商用命令桥的 defaultAction 必须为 deny。");
|
|
return result;
|
|
}
|
|
|
|
public static CommandRolloutPolicy Load(
|
|
string path,
|
|
string expectedSha256,
|
|
string expectedCustomerId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
throw Invalid("命令发布配置路径不能为空。");
|
|
if (string.IsNullOrEmpty(expectedSha256)
|
|
|| !SafeSha256.IsMatch(expectedSha256))
|
|
{
|
|
throw Invalid("命令发布配置 SHA-256 必须是 64 位十六进制值。");
|
|
}
|
|
if (!IsSafeIdentifier(expectedCustomerId, 64))
|
|
throw Invalid("客户部署标识格式无效。");
|
|
|
|
try
|
|
{
|
|
byte[] sourceBytes = ReadRegularFile(path);
|
|
string sourceSha256 = Sha256(sourceBytes);
|
|
if (!FixedTimeEquals(
|
|
sourceSha256,
|
|
expectedSha256.ToLowerInvariant()))
|
|
{
|
|
throw Invalid("命令发布配置与固定 SHA-256 不一致。");
|
|
}
|
|
|
|
JObject root = ParseStrictObject(sourceBytes);
|
|
EnsureOnly(
|
|
root,
|
|
"schemaVersion",
|
|
"customerId",
|
|
"databaseScopeFingerprint",
|
|
"defaultAction",
|
|
"rules");
|
|
string schemaVersion = RequiredString(root, "schemaVersion");
|
|
string customerId = RequiredString(root, "customerId");
|
|
string databaseScopeFingerprint = RequiredString(
|
|
root,
|
|
"databaseScopeFingerprint");
|
|
string defaultAction = RequiredString(root, "defaultAction");
|
|
JArray ruleValues = RequiredArray(root, "rules");
|
|
|
|
if (!string.Equals(schemaVersion, "1.1", StringComparison.Ordinal))
|
|
throw Invalid("命令发布配置 schemaVersion 必须为 1.1。");
|
|
if (!IsSafeIdentifier(customerId, 64)
|
|
|| !string.Equals(
|
|
customerId,
|
|
expectedCustomerId,
|
|
StringComparison.Ordinal))
|
|
{
|
|
throw Invalid("命令发布配置与当前客户部署标识不一致。");
|
|
}
|
|
if (!SafeSha256.IsMatch(databaseScopeFingerprint))
|
|
throw Invalid("命令发布配置数据库作用域指纹无效。");
|
|
databaseScopeFingerprint =
|
|
databaseScopeFingerprint.ToLowerInvariant();
|
|
bool defaultAllow;
|
|
if (string.Equals(defaultAction, "allow", StringComparison.Ordinal))
|
|
defaultAllow = true;
|
|
else if (string.Equals(defaultAction, "deny", StringComparison.Ordinal))
|
|
defaultAllow = false;
|
|
else
|
|
throw Invalid("defaultAction 只允许 allow 或 deny。");
|
|
if (ruleValues.Count > MaximumRules)
|
|
throw Invalid("命令发布规则数量超过上限。");
|
|
|
|
List<CommandRolloutRule> rules = new List<CommandRolloutRule>();
|
|
HashSet<string> commandNames = new HashSet<string>(
|
|
StringComparer.OrdinalIgnoreCase);
|
|
foreach (JToken token in ruleValues)
|
|
{
|
|
JObject ruleObject = token as JObject;
|
|
if (ruleObject == null)
|
|
throw Invalid("每条命令发布规则必须是 JSON 对象。");
|
|
CommandRolloutRule rule = ParseRule(ruleObject);
|
|
if (!commandNames.Add(rule.Command))
|
|
throw Invalid("命令发布配置包含重复命令规则。");
|
|
rules.Add(rule);
|
|
}
|
|
|
|
return new CommandRolloutPolicy(
|
|
true,
|
|
customerId,
|
|
databaseScopeFingerprint,
|
|
sourceSha256,
|
|
defaultAllow,
|
|
rules);
|
|
}
|
|
catch (CommandKernelException)
|
|
{
|
|
throw;
|
|
}
|
|
catch
|
|
{
|
|
throw Invalid("命令发布配置不是有效的严格 UTF-8 JSON。");
|
|
}
|
|
}
|
|
|
|
public void ValidateRegisteredCommands(
|
|
IEnumerable<CommandDescriptor> registeredCommands)
|
|
{
|
|
if (!Configured) return;
|
|
if (registeredCommands == null)
|
|
throw Invalid("无法校验当前命令注册表。");
|
|
|
|
Dictionary<string, CommandDescriptor> descriptors =
|
|
new Dictionary<string, CommandDescriptor>(
|
|
StringComparer.OrdinalIgnoreCase);
|
|
foreach (CommandDescriptor descriptor in registeredCommands)
|
|
{
|
|
if (descriptor == null
|
|
|| string.IsNullOrWhiteSpace(descriptor.Name)
|
|
|| descriptors.ContainsKey(descriptor.Name))
|
|
{
|
|
throw Invalid("当前命令注册表包含无效或重复命令。");
|
|
}
|
|
descriptors.Add(descriptor.Name, descriptor);
|
|
}
|
|
|
|
foreach (CommandRolloutRule rule in _rules.Values)
|
|
{
|
|
CommandDescriptor descriptor;
|
|
if (!descriptors.TryGetValue(rule.Command, out descriptor))
|
|
throw Invalid("命令发布规则引用了当前未注册的命令。");
|
|
if (!string.Equals(
|
|
rule.CommandVersion,
|
|
descriptor.Version ?? string.Empty,
|
|
StringComparison.Ordinal)
|
|
|| !string.Equals(
|
|
rule.RequiredPermission,
|
|
descriptor.RequiredPermission ?? string.Empty,
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw Invalid("命令发布规则与当前命令版本或权限契约不一致。");
|
|
}
|
|
}
|
|
}
|
|
|
|
public CommandAuthorizationDecision Authorize(
|
|
CommandDescriptor descriptor,
|
|
CommandExecutionContext context,
|
|
bool execution)
|
|
{
|
|
if (!Configured) return CommandAuthorizationDecision.Allow();
|
|
if (descriptor == null || context == null)
|
|
return Deny();
|
|
if (!SafeSha256.IsMatch(
|
|
context.DatabaseScopeFingerprint ?? string.Empty)
|
|
|| !FixedTimeEquals(
|
|
DatabaseScopeFingerprint,
|
|
context.DatabaseScopeFingerprint.ToLowerInvariant()))
|
|
{
|
|
return Deny();
|
|
}
|
|
|
|
CommandRolloutRule rule;
|
|
if (!_rules.TryGetValue(descriptor.Name ?? string.Empty, out rule))
|
|
return _defaultAllow
|
|
? CommandAuthorizationDecision.Allow()
|
|
: Deny();
|
|
if (!string.Equals(
|
|
rule.CommandVersion,
|
|
descriptor.Version ?? string.Empty,
|
|
StringComparison.Ordinal)
|
|
|| !string.Equals(
|
|
rule.RequiredPermission,
|
|
descriptor.RequiredPermission ?? string.Empty,
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return Deny();
|
|
}
|
|
if (string.IsNullOrWhiteSpace(context.AccountBook)
|
|
|| string.IsNullOrWhiteSpace(context.SubSystemId)
|
|
|| string.IsNullOrWhiteSpace(context.UserId)
|
|
|| !rule.AccountBooks.Contains(context.AccountBook)
|
|
|| !rule.SubSystemIds.Contains(context.SubSystemId))
|
|
{
|
|
return Deny();
|
|
}
|
|
|
|
if (string.Equals(
|
|
rule.Audience,
|
|
CommandRolloutRule.Administrators,
|
|
StringComparison.Ordinal))
|
|
{
|
|
return AdministratorIdentity.IsBuiltIn(
|
|
context.UserId,
|
|
context.UserName)
|
|
? CommandAuthorizationDecision.Allow()
|
|
: Deny();
|
|
}
|
|
if (string.Equals(
|
|
rule.Audience,
|
|
CommandRolloutRule.Users,
|
|
StringComparison.Ordinal))
|
|
{
|
|
return rule.UserIds.Contains(context.UserId)
|
|
? CommandAuthorizationDecision.Allow()
|
|
: Deny();
|
|
}
|
|
return CommandAuthorizationDecision.Allow();
|
|
}
|
|
|
|
private static CommandRolloutRule ParseRule(JObject value)
|
|
{
|
|
EnsureOnly(
|
|
value,
|
|
"command",
|
|
"commandVersion",
|
|
"requiredPermission",
|
|
"accountBooks",
|
|
"subSystemIds",
|
|
"audience",
|
|
"userIds");
|
|
|
|
string command = RequiredString(value, "command");
|
|
string commandVersion = RequiredString(value, "commandVersion");
|
|
string requiredPermission = RequiredString(
|
|
value,
|
|
"requiredPermission");
|
|
string audience = RequiredString(value, "audience");
|
|
if (!IsSafeIdentifier(command, 128)
|
|
|| !IsSafeIdentifier(commandVersion, 64))
|
|
{
|
|
throw Invalid("命令名或命令版本格式无效。");
|
|
}
|
|
EnsureCleanText(requiredPermission, 160, "requiredPermission");
|
|
if (!string.Equals(
|
|
audience,
|
|
CommandRolloutRule.AllAuthorized,
|
|
StringComparison.Ordinal)
|
|
&& !string.Equals(
|
|
audience,
|
|
CommandRolloutRule.Administrators,
|
|
StringComparison.Ordinal)
|
|
&& !string.Equals(
|
|
audience,
|
|
CommandRolloutRule.Users,
|
|
StringComparison.Ordinal))
|
|
{
|
|
throw Invalid(
|
|
"audience 只允许 all_authorized、administrators 或 users。");
|
|
}
|
|
|
|
CommandRolloutScope accountBooks = ParseScope(
|
|
RequiredObject(value, "accountBooks"),
|
|
true,
|
|
"accountBooks");
|
|
CommandRolloutScope subSystemIds = ParseScope(
|
|
RequiredObject(value, "subSystemIds"),
|
|
false,
|
|
"subSystemIds");
|
|
HashSet<string> userIds = ParseStringSet(
|
|
RequiredArray(value, "userIds"),
|
|
false,
|
|
"userIds");
|
|
if (string.Equals(
|
|
audience,
|
|
CommandRolloutRule.Users,
|
|
StringComparison.Ordinal))
|
|
{
|
|
if (userIds.Count == 0)
|
|
throw Invalid("users audience 必须提供至少一个 userId。");
|
|
}
|
|
else if (userIds.Count != 0)
|
|
{
|
|
throw Invalid("非 users audience 不允许配置 userIds。");
|
|
}
|
|
|
|
return new CommandRolloutRule
|
|
{
|
|
Command = command,
|
|
CommandVersion = commandVersion,
|
|
RequiredPermission = requiredPermission,
|
|
AccountBooks = accountBooks,
|
|
SubSystemIds = subSystemIds,
|
|
Audience = audience,
|
|
UserIds = userIds
|
|
};
|
|
}
|
|
|
|
private static CommandRolloutScope ParseScope(
|
|
JObject value,
|
|
bool allowUnicode,
|
|
string fieldName)
|
|
{
|
|
EnsureOnly(value, "all", "values");
|
|
bool all = RequiredBoolean(value, "all");
|
|
HashSet<string> values = ParseStringSet(
|
|
RequiredArray(value, "values"),
|
|
allowUnicode,
|
|
fieldName + ".values");
|
|
if (all && values.Count != 0)
|
|
throw Invalid(fieldName + " 在 all=true 时 values 必须为空。");
|
|
if (!all && values.Count == 0)
|
|
throw Invalid(fieldName + " 在 all=false 时 values 不能为空。");
|
|
return new CommandRolloutScope(all, values);
|
|
}
|
|
|
|
private static HashSet<string> ParseStringSet(
|
|
JArray values,
|
|
bool allowUnicode,
|
|
string fieldName)
|
|
{
|
|
if (values.Count > MaximumScopeValues)
|
|
throw Invalid(fieldName + " 数量超过上限。");
|
|
HashSet<string> result = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (JToken token in values)
|
|
{
|
|
if (token.Type != JTokenType.String)
|
|
throw Invalid(fieldName + " 只允许字符串值。");
|
|
string item = (string)token;
|
|
if (allowUnicode)
|
|
EnsureCleanText(item, 128, fieldName);
|
|
else if (!IsSafeIdentifier(item, 128))
|
|
throw Invalid(fieldName + " 包含格式无效的标识符。");
|
|
if (!result.Add(item))
|
|
throw Invalid(fieldName + " 包含重复值。");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static byte[] ReadRegularFile(string path)
|
|
{
|
|
string fullPath = Path.GetFullPath(path);
|
|
FileInfo file = new FileInfo(fullPath);
|
|
if (!file.Exists)
|
|
throw Invalid("命令发布配置文件不存在。");
|
|
FileAttributes attributes = file.Attributes;
|
|
if ((attributes & (FileAttributes.Directory
|
|
| FileAttributes.Device
|
|
| FileAttributes.ReparsePoint)) != 0)
|
|
{
|
|
throw Invalid("命令发布配置必须是普通文件,不能是目录、设备或链接。");
|
|
}
|
|
|
|
using (FileStream stream = new FileStream(
|
|
fullPath,
|
|
FileMode.Open,
|
|
FileAccess.Read,
|
|
FileShare.Read))
|
|
{
|
|
if (stream.Length <= 0 || stream.Length > MaximumBytes)
|
|
throw Invalid("命令发布配置为空或超过大小上限。");
|
|
byte[] result = new byte[(int)stream.Length];
|
|
int offset = 0;
|
|
while (offset < result.Length)
|
|
{
|
|
int count = stream.Read(result, offset, result.Length - offset);
|
|
if (count <= 0)
|
|
throw Invalid("命令发布配置读取不完整。");
|
|
offset += count;
|
|
}
|
|
if (stream.ReadByte() != -1)
|
|
throw Invalid("命令发布配置读取期间发生变化。");
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private static JObject ParseStrictObject(byte[] sourceBytes)
|
|
{
|
|
string source = new UTF8Encoding(false, true).GetString(sourceBytes);
|
|
if (!StrictRuntimeJsonSyntax.IsStandard(source))
|
|
throw Invalid("命令发布配置必须使用无 BOM、无注释、无尾逗号的标准 JSON。");
|
|
using (StringReader input = new StringReader(source))
|
|
using (JsonTextReader reader = new JsonTextReader(input))
|
|
{
|
|
reader.DateParseHandling = DateParseHandling.None;
|
|
reader.FloatParseHandling = FloatParseHandling.Decimal;
|
|
reader.MaxDepth = 32;
|
|
while (reader.Read())
|
|
{
|
|
if (reader.TokenType == JsonToken.Comment)
|
|
throw Invalid("命令发布配置不允许 JSON 注释。");
|
|
}
|
|
}
|
|
return JObject.Parse(
|
|
source,
|
|
new JsonLoadSettings
|
|
{
|
|
DuplicatePropertyNameHandling =
|
|
DuplicatePropertyNameHandling.Error,
|
|
CommentHandling = CommentHandling.Ignore,
|
|
LineInfoHandling = LineInfoHandling.Ignore
|
|
});
|
|
}
|
|
|
|
private static JObject RequiredObject(JObject value, string name)
|
|
{
|
|
JToken token = value[name];
|
|
JObject result = token as JObject;
|
|
if (result == null)
|
|
throw Invalid(name + " 必须是 JSON 对象。");
|
|
return result;
|
|
}
|
|
|
|
private static JArray RequiredArray(JObject value, string name)
|
|
{
|
|
JToken token = value[name];
|
|
JArray result = token as JArray;
|
|
if (result == null)
|
|
throw Invalid(name + " 必须是 JSON 数组。");
|
|
return result;
|
|
}
|
|
|
|
private static string RequiredString(JObject value, string name)
|
|
{
|
|
JToken token = value[name];
|
|
if (token == null || token.Type != JTokenType.String)
|
|
throw Invalid(name + " 必须是字符串。");
|
|
return (string)token;
|
|
}
|
|
|
|
private static bool RequiredBoolean(JObject value, string name)
|
|
{
|
|
JToken token = value[name];
|
|
if (token == null || token.Type != JTokenType.Boolean)
|
|
throw Invalid(name + " 必须是布尔值。");
|
|
return (bool)token;
|
|
}
|
|
|
|
private static void EnsureOnly(JObject value, params string[] allowed)
|
|
{
|
|
HashSet<string> names = new HashSet<string>(
|
|
allowed,
|
|
StringComparer.Ordinal);
|
|
JProperty unknown = value.Properties().FirstOrDefault(
|
|
item => !names.Contains(item.Name));
|
|
if (unknown != null)
|
|
throw Invalid("命令发布配置包含未知字段:" + unknown.Name);
|
|
}
|
|
|
|
private static bool IsSafeIdentifier(string value, int maximumLength)
|
|
{
|
|
return !string.IsNullOrEmpty(value)
|
|
&& value.Length <= maximumLength
|
|
&& SafeIdentifier.IsMatch(value);
|
|
}
|
|
|
|
private static void EnsureCleanText(
|
|
string value,
|
|
int maximumLength,
|
|
string fieldName)
|
|
{
|
|
if (string.IsNullOrEmpty(value)
|
|
|| value.Length > maximumLength
|
|
|| !string.Equals(value, value.Trim(), StringComparison.Ordinal))
|
|
{
|
|
throw Invalid(fieldName + " 为空、过长或包含首尾空白。");
|
|
}
|
|
foreach (char item in value)
|
|
{
|
|
UnicodeCategory category = char.GetUnicodeCategory(item);
|
|
if (char.IsControl(item)
|
|
|| char.IsSurrogate(item)
|
|
|| category == UnicodeCategory.Format
|
|
|| category == UnicodeCategory.LineSeparator
|
|
|| category == UnicodeCategory.ParagraphSeparator)
|
|
{
|
|
throw Invalid(fieldName + " 包含不允许的控制或格式字符。");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string Sha256(byte[] value)
|
|
{
|
|
using (SHA256 sha = SHA256.Create())
|
|
{
|
|
byte[] hash = sha.ComputeHash(value);
|
|
StringBuilder result = new StringBuilder(hash.Length * 2);
|
|
foreach (byte item in hash) result.Append(item.ToString("x2"));
|
|
return result.ToString();
|
|
}
|
|
}
|
|
|
|
private static bool FixedTimeEquals(string left, string right)
|
|
{
|
|
if (left == null || right == null || left.Length != right.Length)
|
|
return false;
|
|
int difference = 0;
|
|
for (int index = 0; index < left.Length; index++)
|
|
difference |= left[index] ^ right[index];
|
|
return difference == 0;
|
|
}
|
|
|
|
private static CommandAuthorizationDecision Deny()
|
|
{
|
|
return CommandAuthorizationDecision.Deny(
|
|
"command_rollout_denied",
|
|
"该命令不在当前客户 ERP 会话的发布范围内。");
|
|
}
|
|
|
|
private static CommandKernelException Invalid(string message)
|
|
{
|
|
return new CommandKernelException(
|
|
"command_rollout_policy_invalid",
|
|
message,
|
|
6);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Json.NET intentionally accepts several JavaScript extensions. Runtime
|
|
/// configuration files are human-reviewed and hash-pinned, so accepting a
|
|
/// BOM, comments, trailing commas or non-JSON whitespace would make the
|
|
/// reviewed text contract ambiguous. The normal Json.NET parser still
|
|
/// performs the complete structural/type validation after this bounded
|
|
/// lexical gate.
|
|
/// </summary>
|
|
internal static class StrictRuntimeJsonSyntax
|
|
{
|
|
public static bool IsStandard(string source)
|
|
{
|
|
if (string.IsNullOrEmpty(source) || source[0] == '\uFEFF')
|
|
return false;
|
|
|
|
bool inString = false;
|
|
bool escaped = false;
|
|
char previousSignificant = '\0';
|
|
for (int index = 0; index < source.Length; index++)
|
|
{
|
|
char current = source[index];
|
|
if (inString)
|
|
{
|
|
if (escaped)
|
|
{
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (current == '\\')
|
|
{
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (current == '"')
|
|
{
|
|
inString = false;
|
|
continue;
|
|
}
|
|
if (current < 0x20)
|
|
return false;
|
|
continue;
|
|
}
|
|
|
|
if (current == '"')
|
|
{
|
|
inString = true;
|
|
previousSignificant = current;
|
|
continue;
|
|
}
|
|
if (current == '/'
|
|
&& index + 1 < source.Length
|
|
&& (source[index + 1] == '/'
|
|
|| source[index + 1] == '*'))
|
|
return false;
|
|
if ((current == '}' || current == ']')
|
|
&& previousSignificant == ',')
|
|
return false;
|
|
if (current == ' ' || current == '\t'
|
|
|| current == '\r' || current == '\n')
|
|
continue;
|
|
if (char.IsWhiteSpace(current))
|
|
return false;
|
|
previousSignificant = current;
|
|
}
|
|
return !inString && !escaped;
|
|
}
|
|
}
|
|
|
|
internal sealed class CommandRolloutScope
|
|
{
|
|
private readonly HashSet<string> _values;
|
|
|
|
public CommandRolloutScope(bool all, HashSet<string> values)
|
|
{
|
|
All = all;
|
|
_values = values ?? new HashSet<string>(StringComparer.Ordinal);
|
|
}
|
|
|
|
public bool All { get; private set; }
|
|
|
|
public bool Contains(string value)
|
|
{
|
|
return All || _values.Contains(value ?? string.Empty);
|
|
}
|
|
}
|
|
|
|
internal sealed class CommandRolloutRule
|
|
{
|
|
public const string AllAuthorized = "all_authorized";
|
|
public const string Administrators = "administrators";
|
|
public const string Users = "users";
|
|
|
|
public string Command { get; set; }
|
|
public string CommandVersion { get; set; }
|
|
public string RequiredPermission { get; set; }
|
|
public CommandRolloutScope AccountBooks { get; set; }
|
|
public CommandRolloutScope SubSystemIds { get; set; }
|
|
public string Audience { get; set; }
|
|
public HashSet<string> UserIds { get; set; }
|
|
}
|
|
}
|