[#30] Client: Add object model for Rules
All checks were successful
DCO / DCO (pull_request) Successful in 27s
lint-build / dotnet8.0 (pull_request) Successful in 43s
lint-build / dotnet8.0 (push) Successful in 46s

Signed-off-by: Pavel Gross <p.gross@yadro.com>
This commit is contained in:
Pavel Gross 2025-02-12 04:20:01 +03:00
parent 43e300c773
commit 195854a45b
27 changed files with 677 additions and 155 deletions

View file

@ -0,0 +1,36 @@
namespace FrostFS.SDK.Client;
public struct Actions(bool inverted, string[] names) : System.IEquatable<Actions>
{
public bool Inverted { get; set; } = inverted;
public string[] Names { get; set; } = names;
public override bool Equals(object obj)
{
if (obj == null || obj is not Actions)
return false;
return Equals((Actions)obj);
}
public override readonly int GetHashCode()
{
return Inverted.GetHashCode() ^ string.Join(string.Empty, Names).GetHashCode();
}
public static bool operator ==(Actions left, Actions right)
{
return left.Equals(right);
}
public static bool operator !=(Actions left, Actions right)
{
return !(left == right);
}
public readonly bool Equals(Actions other)
{
return this.GetHashCode().Equals(other.GetHashCode());
}
}

View file

@ -0,0 +1,43 @@
namespace FrostFS.SDK.Client;
public struct Condition : System.IEquatable<Condition>
{
public ConditionType Op { get; set; }
public ConditionKindType Kind { get; set; }
public string? Key { get; set; }
public string? Value { get; set; }
public override bool Equals(object obj)
{
if (obj == null || obj is not Condition)
return false;
return Equals((Condition)obj);
}
public override readonly int GetHashCode()
{
return Op.GetHashCode()
^ Kind.GetHashCode()
^ (Key != null ? Key.GetHashCode() : 0)
^ (Value != null ? Value.GetHashCode() : 0);
}
public static bool operator ==(Condition left, Condition right)
{
return left.Equals(right);
}
public static bool operator !=(Condition left, Condition right)
{
return !(left == right);
}
public readonly bool Equals(Condition other)
{
return this.GetHashCode().Equals(other.GetHashCode());
}
}

View file

@ -0,0 +1,7 @@
namespace FrostFS.SDK.Client;
public enum ConditionKindType
{
Resource,
Request
}

View file

@ -0,0 +1,36 @@
namespace FrostFS.SDK.Client;
public enum ConditionType
{
CondStringEquals,
CondStringNotEquals,
CondStringEqualsIgnoreCase,
CondStringNotEqualsIgnoreCase,
CondStringLike,
CondStringNotLike,
CondStringLessThan,
CondStringLessThanEquals,
CondStringGreaterThan,
CondStringGreaterThanEquals,
// Numeric condition operators.
CondNumericEquals,
CondNumericNotEquals,
CondNumericLessThan,
CondNumericLessThanEquals,
CondNumericGreaterThan,
CondNumericGreaterThanEquals,
CondSliceContains,
CondIPAddress,
CondNotIPAddress,
}

View file

@ -2,7 +2,7 @@
public enum FrostFsTargetType
{
Undefined = 0,
Undefined,
Namespace,
Container,
User,

View file

@ -0,0 +1,9 @@
namespace FrostFS.SDK.Client;
public enum RuleStatus
{
Allow,
NoRuleFound,
AccessDenied,
QuotaLimitReached
}

View file

@ -0,0 +1,10 @@
namespace FrostFS.SDK.Client;
public class FrostFsChain
{
public byte[] ID { get; set; } = [];
public FrostFsRule[] Rules { get; set; } = [];
public RuleMatchType MatchType { get; set; }
}

View file

@ -0,0 +1,18 @@
namespace FrostFS.SDK.Client;
public class FrostFsRule
{
public RuleStatus Status { get; set; }
// Actions the operation is applied to.
public Actions Actions { get; set; }
// List of the resources the operation is applied to.
public Resource Resources { get; set; }
// True iff individual conditions must be combined with the logical OR.
// By default AND is used, so _each_ condition must pass.
public bool Any { get; set; }
public Condition[]? Conditions { get; set; }
}

View file

@ -0,0 +1,10 @@
namespace FrostFS.SDK.Client;
public enum RuleMatchType
{
// DenyPriority rejects the request if any `Deny` is specified.
DenyPriority,
// FirstMatch returns the first rule action matched to the request.
FirstMatch
}

View file

@ -0,0 +1,36 @@
namespace FrostFS.SDK.Client;
public struct Resource(bool inverted, string[] names) : System.IEquatable<Resource>
{
public bool Inverted { get; set; } = inverted;
public string[] Names { get; set; } = names;
public override bool Equals(object obj)
{
if (obj == null || obj is not Resource)
return false;
return Equals((Resource)obj);
}
public override readonly int GetHashCode()
{
return Inverted.GetHashCode() ^ string.Join(string.Empty, Names).GetHashCode();
}
public static bool operator ==(Resource left, Resource right)
{
return left.Equals(right);
}
public static bool operator !=(Resource left, Resource right)
{
return !(left == right);
}
public readonly bool Equals(Resource other)
{
return this.GetHashCode().Equals(other.GetHashCode());
}
}

View file

@ -0,0 +1,286 @@
using System;
namespace FrostFS.SDK.Client;
internal static class RuleSerializer
{
const byte Version = 0; // increase if breaking change
const int ByteSize = 1;
const int UInt8Size = ByteSize;
const int BoolSize = ByteSize;
const long NullSlice = -1;
const int NullSliceSize = 1;
const byte ByteTrue = 1;
const byte ByteFalse = 0;
/// <summary>
/// maxSliceLen taken from https://github.com/neo-project/neo/blob/38218bbee5bbe8b33cd8f9453465a19381c9a547/src/Neo/IO/Helper.cs#L77
/// </summary>
const int MaxSliceLen = 0x1000000;
const int ChainMarshalVersion = 0;
internal static byte[] Serialize(FrostFsChain chain)
{
int s = UInt8Size // Marshaller version
+ UInt8Size // Chain version
+ SliceSize(chain.ID, b => ByteSize)
+ SliceSize(chain.Rules, RuleSize)
+ UInt8Size; // MatchType
byte[] buf = new byte[s];
int offset = UInt8Marshal(buf, 0, Version);
offset = UInt8Marshal(buf, offset, ChainMarshalVersion);
offset = SliceMarshal(buf, offset, chain.ID, ByteMarshal);
offset = SliceMarshal(buf, offset, chain.Rules, MarshalRule);
offset = UInt8Marshal(buf, offset, (byte)chain.MatchType);
VerifyMarshal(buf, offset);
return buf;
}
private static int Int64Size(long value)
{
// https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=92;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c
// and
// https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=41;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c
ulong ux = (ulong)value << 1;
if (value < 0)
{
ux = ~ux;
}
int size = 0;
while (ux >= 0x80)
{
size++;
ux >>= 7;
}
return size + 1;
}
private static int SliceSize<T>(T[] slice, Func<T, int> sizeOf)
{
if (slice == null)
{
return NullSliceSize;
}
// Assuming Int64Size is the size of the slice
var size = Int64Size(slice.Length);
foreach (var v in slice)
{
size += sizeOf(v);
}
return size;
}
private static int StringSize(string? s)
{
var len = s !=null ? s.Length : 0;
return Int64Size(len) + len;
}
private static int ActionsSize(Actions action)
{
return BoolSize // Inverted
+ SliceSize(action.Names, StringSize);
}
private static int ResourcesSize(Resource resource)
{
return BoolSize // Inverted
+ SliceSize(resource.Names, StringSize);
}
private static int ConditionSize(Condition condition)
{
return ByteSize // Op
+ ByteSize // Object
+ StringSize(condition.Key)
+ StringSize(condition.Value);
}
public static int RuleSize(FrostFsRule rule)
{
if (rule is null)
{
throw new ArgumentNullException(nameof(rule));
}
return ByteSize // Status
+ ActionsSize(rule.Actions)
+ ResourcesSize(rule.Resources)
+ BoolSize // Any
+ SliceSize(rule.Conditions!, ConditionSize);
}
public static int UInt8Marshal(byte[] buf, int offset, byte value)
{
if (buf.Length - offset < 1)
{
throw new FrostFsException("Not enough bytes left to serialize value of type byte");
}
buf[offset] = value;
return offset + 1;
}
public static int ByteMarshal(byte[] buf, int offset, byte value)
{
return UInt8Marshal(buf, offset, value);
}
// PutVarint encodes an int64 into buf and returns the number of bytes written.
// If the buffer is too small, PutVarint will panic.
private static int PutVarint(byte[] buf, int offset, long x)
{
var ux = (ulong)x << 1;
if (x < 0)
{
ux = ~ux;
}
return PutUvarint(buf, offset, ux);
}
private static int PutUvarint(byte[] buf, int offset, ulong x)
{
while (x >= 0x80)
{
buf[offset] = (byte)(x | 0x80);
x >>= 7;
offset++;
}
buf[offset] = (byte)x;
return offset + 1;
}
public static int Int64Marshal(byte[] buf, int offset, long v)
{
if (buf.Length - offset < Int64Size(v))
{
throw new FrostFsException("Not enough bytes left to serialize value of type long");
}
return PutVarint(buf, offset, v);
}
public static int SliceMarshal<T>(byte[] buf, int offset, T[] slice, Func<byte[], int, T, int> marshalT)
{
if (slice == null)
{
return Int64Marshal(buf, offset, NullSlice);
}
if (slice.Length > MaxSliceLen)
{
throw new FrostFsException($"slice size if too big: {slice.Length}");
}
offset = Int64Marshal(buf, offset, slice.Length);
foreach (var v in slice)
{
offset = marshalT(buf, offset, v);
}
return offset;
}
private static int BoolMarshal(byte[] buf, int offset, bool value)
{
return UInt8Marshal(buf, offset, value ? ByteTrue : ByteFalse);
}
private static int StringMarshal(byte[] buf, int offset, string value)
{
if (value == null)
{
throw new FrostFsException($"string value is null");
}
if (value.Length > MaxSliceLen)
{
throw new FrostFsException($"string is too long: {value.Length}");
}
if (buf.Length - offset < Int64Size(value.Length) + value.Length)
{
throw new FrostFsException($"Not enough bytes left to serialize value of type string with length {value.Length}");
}
offset = Int64Marshal(buf, offset, value.Length);
if (string.IsNullOrEmpty(value))
{
return offset;
}
Buffer.BlockCopy(System.Text.Encoding.UTF8.GetBytes(value), 0, buf, offset, value.Length);
return offset + value.Length;
}
private static int MarshalActions(byte[] buf, int offset, Actions action)
{
offset = BoolMarshal(buf, offset, action.Inverted);
return SliceMarshal(buf, offset, action.Names, StringMarshal);
}
private static int MarshalCondition(byte[] buf, int offset, Condition condition)
{
offset = ByteMarshal(buf, offset, (byte)condition.Op);
offset = ByteMarshal(buf, offset, (byte)condition.Kind);
offset = StringMarshal(buf, offset, condition.Key!);
return StringMarshal(buf, offset, condition.Value!);
}
private static int MarshalRule(byte[] buf, int offset, FrostFsRule rule)
{
if (rule is null)
{
throw new ArgumentNullException(nameof(rule));
}
offset = ByteMarshal(buf, offset, (byte)rule.Status);
offset = MarshalActions(buf, offset, rule.Actions);
offset = MarshalResources(buf, offset, rule.Resources);
offset = BoolMarshal(buf, offset, rule.Any);
return SliceMarshal(buf, offset, rule.Conditions!, MarshalCondition);
}
private static int MarshalResources(byte[] buf, int offset, Resource resources)
{
offset = BoolMarshal(buf, offset, resources.Inverted);
return SliceMarshal(buf, offset, resources.Names, StringMarshal);
}
private static void VerifyMarshal(byte[] buf, int lastOffset)
{
if (buf.Length != lastOffset)
{
throw new FrostFsException("actual data size differs from expected");
}
}
}

View file

@ -10,7 +10,8 @@ public class FrostFsResponseException : FrostFsException
{
}
public FrostFsResponseException(FrostFsResponseStatus status)
public FrostFsResponseException(FrostFsResponseStatus status)
: base(status != null ? status.Message != null ? "" : "" : "")
{
Status = status;
}

View file

@ -1,4 +1,5 @@
using System;
using System.Linq;
namespace FrostFS.SDK.Client.Mappers.GRPC;
@ -13,6 +14,9 @@ public static class StatusMapper
return codeName is null
? throw new ArgumentException($"Unknown StatusCode. Value: '{status.Code}'.")
: new FrostFsResponseStatus((FrostFsStatusCode)status.Code, status.Message);
: new FrostFsResponseStatus(
(FrostFsStatusCode)status.Code,
status.Message,
string.Join(", ", status.Details.Select(d => System.Text.Encoding.UTF8.GetString([.. d.Value]))));
}
}

View file

@ -1,41 +0,0 @@
using Google.Protobuf;
namespace FrostFS.SDK.Client;
public struct FrostFsChain(byte[] raw) : System.IEquatable<FrostFsChain>
{
private ByteString? grpcRaw;
public byte[] Raw { get; } = raw;
internal ByteString GetRaw()
{
return grpcRaw ??= ByteString.CopyFrom(Raw);
}
public override readonly bool Equals(object obj)
{
var chain = (FrostFsChain)obj;
return Equals(chain);
}
public override readonly int GetHashCode()
{
return Raw.GetHashCode();
}
public static bool operator ==(FrostFsChain left, FrostFsChain right)
{
return left.Equals(right);
}
public static bool operator !=(FrostFsChain left, FrostFsChain right)
{
return !(left == right);
}
public readonly bool Equals(FrostFsChain other)
{
return Raw == other.Raw;
}
}

View file

@ -34,21 +34,18 @@ public class FrostFsContainerId
throw new FrostFsInvalidObjectException();
}
internal ContainerID ContainerID
public ContainerID GetContainerID()
{
get
if (this.containerID != null)
return this.containerID;
if (modelId != null)
{
if (this.containerID != null)
return this.containerID;
if (modelId != null)
{
this.containerID = this.ToMessage();
return this.containerID;
}
throw new FrostFsInvalidObjectException();
this.containerID = this.ToMessage();
return this.containerID;
}
throw new FrostFsInvalidObjectException();
}
public override string ToString()

View file

@ -1,14 +1,16 @@
namespace FrostFS.SDK;
public class FrostFsResponseStatus(FrostFsStatusCode code, string? message = null)
public class FrostFsResponseStatus(FrostFsStatusCode code, string? message = null, string? details = null)
{
public FrostFsStatusCode Code { get; set; } = code;
public string Message { get; set; } = message ?? string.Empty;
public string Details { get; set; } = details ?? string.Empty;
public bool IsSuccess => Code == FrostFsStatusCode.Success;
public override string ToString()
{
return $"Response status: {Code}. Message: {Message}.";
return $"Response status: {Code}. Message: {Message}. Details: {Details}";
}
}

View file

@ -1,4 +1,6 @@
namespace FrostFS.SDK.Client;
using FrostFS.SDK.Client;
namespace FrostFS.SDK.Client;
public readonly struct PrmApeChainAdd(FrostFsChainTarget target, FrostFsChain chain, string[]? xheaders = null) : System.IEquatable<PrmApeChainAdd>
{

View file

@ -1,4 +1,6 @@
namespace FrostFS.SDK.Client;
using FrostFS.SDK.Client;
namespace FrostFS.SDK.Client;
public readonly struct PrmApeChainList(FrostFsChainTarget target, string[]? xheaders = null) : System.IEquatable<PrmApeChainList>
{

View file

@ -1,13 +1,16 @@
namespace FrostFS.SDK.Client;
using System;
using FrostFS.SDK.Client;
namespace FrostFS.SDK.Client;
public readonly struct PrmApeChainRemove(
FrostFsChainTarget target,
FrostFsChain chain,
byte[] chainId,
string[]? xheaders = null) : System.IEquatable<PrmApeChainRemove>
{
public FrostFsChainTarget Target { get; } = target;
public FrostFsChain Chain { get; } = chain;
public byte[] ChainId { get; } = chainId;
/// <summary>
/// FrostFS request X-Headers
@ -25,13 +28,13 @@ public readonly struct PrmApeChainRemove(
public readonly bool Equals(PrmApeChainRemove other)
{
return Target == other.Target
&& Chain == other.Chain
&& ChainId.Equals(other.ChainId)
&& XHeaders == other.XHeaders;
}
public override readonly int GetHashCode()
{
return Chain.GetHashCode() ^ Target.GetHashCode() ^ XHeaders.GetHashCode();
return ChainId.GetHashCode() ^ Target.GetHashCode() ^ XHeaders.GetHashCode();
}
public static bool operator ==(PrmApeChainRemove left, PrmApeChainRemove right)

View file

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Frostfs.V2.Ape;
using Frostfs.V2.Apemanager;
using Google.Protobuf;
namespace FrostFS.SDK.Client.Services;
@ -18,11 +19,15 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor
internal async Task<ReadOnlyMemory<byte>> AddChainAsync(PrmApeChainAdd args, CallContext ctx)
{
var binary = RuleSerializer.Serialize(args.Chain);
var base64 = Convert.ToBase64String(binary);
AddChainRequest request = new()
{
Body = new()
{
Chain = new() { Raw = args.Chain.GetRaw() },
Chain = new() { Raw = UnsafeByteOperations.UnsafeWrap(binary) },
Target = args.Target.GetChainTarget()
}
};
@ -43,7 +48,7 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor
{
Body = new()
{
ChainId = args.Chain.GetRaw(),
ChainId = UnsafeByteOperations.UnsafeWrap(args.ChainId),
Target = args.Target.GetChainTarget()
}
};

View file

@ -39,7 +39,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
internal async Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args, CallContext ctx)
{
GetRequest request = GetContainerRequest(args.Container.ContainerID, args.XHeaders, ClientContext.Key.ECDsaKey);
GetRequest request = GetContainerRequest(args.Container.GetContainerID(), args.XHeaders, ClientContext.Key.ECDsaKey);
var response = await service.GetAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);

View file

@ -52,7 +52,7 @@ internal sealed class ObjectServiceProvider(ObjectService.ObjectServiceClient cl
{
Address = new Address
{
ContainerId = args.ContainerId.ContainerID,
ContainerId = args.ContainerId.GetContainerID(),
ObjectId = args.ObjectId.ToMessage()
},
Raw = args.Raw
@ -435,7 +435,10 @@ internal sealed class ObjectServiceProvider(ObjectService.ObjectServiceClient cl
// send the last part and create linkObject
if (sentObjectIds.Count > 0)
{
var largeObjectHeader = new FrostFsObjectHeader(header.ContainerId, FrostFsObjectType.Regular, [.. attributes])
var largeObjectHeader = new FrostFsObjectHeader(
header.ContainerId,
FrostFsObjectType.Regular,
attributes != null ? [.. attributes] : [])
{
PayloadLength = args.PutObjectContext.FullLength,
};
@ -581,7 +584,7 @@ internal sealed class ObjectServiceProvider(ObjectService.ObjectServiceClient cl
}
};
var sessionToken = (args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false));
var sessionToken = args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false);
var protoToken = sessionToken.CreateObjectTokenContext(
new Address { ContainerId = grpcHeader.ContainerId, ObjectId = oid },

View file

@ -88,7 +88,6 @@ public static class Verifier
return key.VerifyData(data2Verify, sig.Sign.ToByteArray());
}
internal static bool VerifyMatryoskaLevel(IMessage body, IMetaHeader meta, IVerificationHeader verification)
{
if (!verification.MetaSignature.VerifyMessagePart(meta))
@ -118,12 +117,19 @@ public static class Verifier
internal static void CheckResponse(IResponse resp)
{
if (!resp.Verify())
{
throw new FormatException($"invalid response, type={resp.GetType()}");
}
var status = resp.MetaHeader.Status.ToModel();
if (resp.MetaHeader != null)
{
var status = resp.MetaHeader.Status.ToModel();
if (status != null && !status.IsSuccess)
throw new FrostFsResponseException(status);
if (status != null && !status.IsSuccess)
{
throw new FrostFsResponseException(status);
}
}
}
/// <summary>
@ -138,6 +144,8 @@ public static class Verifier
}
if (!request.Verify())
{
throw new FrostFsResponseException($"invalid response, type={request.GetType()}");
}
}
}