diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bcf58d..033855d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.6.0] - 2025-02-13 + +### Added +- APE rules serializer + ## [0.5.0] - 2025-02-11 ### Fixed diff --git a/client/src/main/java/info/frostfs/sdk/FrostFSClient.java b/client/src/main/java/info/frostfs/sdk/FrostFSClient.java index e6704ed..0026285 100644 --- a/client/src/main/java/info/frostfs/sdk/FrostFSClient.java +++ b/client/src/main/java/info/frostfs/sdk/FrostFSClient.java @@ -1,7 +1,6 @@ package info.frostfs.sdk; import frostfs.accounting.Types; -import info.frostfs.sdk.dto.chain.Chain; import info.frostfs.sdk.dto.container.Container; import info.frostfs.sdk.dto.container.ContainerId; import info.frostfs.sdk.dto.netmap.NetmapSnapshot; @@ -194,7 +193,7 @@ public class FrostFSClient implements CommonClient { } @Override - public List listChains(PrmApeChainList args, CallContext ctx) { + public List listChains(PrmApeChainList args, CallContext ctx) { return apeManagerClient.listChains(args, ctx); } diff --git a/client/src/main/java/info/frostfs/sdk/constants/RuleConst.java b/client/src/main/java/info/frostfs/sdk/constants/RuleConst.java new file mode 100644 index 0000000..78e656f --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/constants/RuleConst.java @@ -0,0 +1,27 @@ +package info.frostfs.sdk.constants; + +public class RuleConst { + public static final byte VERSION = 0; + + public static final int BYTE_SIZE = 1; + public static final int U_INT_8_SIZE = BYTE_SIZE; + public static final int BOOL_SIZE = BYTE_SIZE; + + public static final long NULL_SLICE = -1L; + public static final int NULL_SLICE_SIZE = 1; + + public static final byte BYTE_TRUE = 1; + public static final byte BYTE_FALSE = 0; + + // maxSliceLen taken from + // https://github.com/neo-project/neo/blob/38218bbee5bbe8b33cd8f9453465a19381c9a547/src/Neo/IO/Helper.cs#L77 + public static final int MAX_SLICE_LENGTH = 0x1000000; + + public static final int CHAIN_MARSHAL_VERSION = 0; + + public static final long OFFSET128 = 0x80; + public static final long UNSIGNED_SERIALIZE_SIZE = 7; + + private RuleConst() { + } +} diff --git a/client/src/main/java/info/frostfs/sdk/jdo/parameters/ape/PrmApeChainAdd.java b/client/src/main/java/info/frostfs/sdk/jdo/parameters/ape/PrmApeChainAdd.java index 4692ac2..e7ae05c 100644 --- a/client/src/main/java/info/frostfs/sdk/jdo/parameters/ape/PrmApeChainAdd.java +++ b/client/src/main/java/info/frostfs/sdk/jdo/parameters/ape/PrmApeChainAdd.java @@ -1,7 +1,7 @@ package info.frostfs.sdk.jdo.parameters.ape; import info.frostfs.sdk.annotations.NotNull; -import info.frostfs.sdk.dto.chain.Chain; +import info.frostfs.sdk.dto.ape.Chain; import info.frostfs.sdk.dto.chain.ChainTarget; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/client/src/main/java/info/frostfs/sdk/jdo/parameters/ape/PrmApeChainRemove.java b/client/src/main/java/info/frostfs/sdk/jdo/parameters/ape/PrmApeChainRemove.java index 93f53d3..00bc2e9 100644 --- a/client/src/main/java/info/frostfs/sdk/jdo/parameters/ape/PrmApeChainRemove.java +++ b/client/src/main/java/info/frostfs/sdk/jdo/parameters/ape/PrmApeChainRemove.java @@ -1,7 +1,6 @@ package info.frostfs.sdk.jdo.parameters.ape; import info.frostfs.sdk.annotations.NotNull; -import info.frostfs.sdk.dto.chain.Chain; import info.frostfs.sdk.dto.chain.ChainTarget; import lombok.AllArgsConstructor; import lombok.Builder; @@ -14,14 +13,15 @@ import java.util.Map; @AllArgsConstructor public class PrmApeChainRemove { @NotNull - private Chain chain; + private byte[] chainId; + @NotNull private ChainTarget chainTarget; private Map xHeaders; - public PrmApeChainRemove(Chain chain, ChainTarget chainTarget) { - this.chain = chain; + public PrmApeChainRemove(byte[] chainId, ChainTarget chainTarget) { + this.chainId = chainId; this.chainTarget = chainTarget; } } diff --git a/client/src/main/java/info/frostfs/sdk/pool/Pool.java b/client/src/main/java/info/frostfs/sdk/pool/Pool.java index 45b818e..d8bac59 100644 --- a/client/src/main/java/info/frostfs/sdk/pool/Pool.java +++ b/client/src/main/java/info/frostfs/sdk/pool/Pool.java @@ -1,7 +1,6 @@ package info.frostfs.sdk.pool; import frostfs.refs.Types; -import info.frostfs.sdk.dto.chain.Chain; import info.frostfs.sdk.dto.container.Container; import info.frostfs.sdk.dto.container.ContainerId; import info.frostfs.sdk.dto.netmap.NetmapSnapshot; @@ -515,7 +514,7 @@ public class Pool implements CommonClient { } @Override - public List listChains(PrmApeChainList args, CallContext ctx) { + public List listChains(PrmApeChainList args, CallContext ctx) { ClientWrapper client = connection(); return client.getClient().listChains(args, ctx); } diff --git a/client/src/main/java/info/frostfs/sdk/services/ApeManagerClient.java b/client/src/main/java/info/frostfs/sdk/services/ApeManagerClient.java index 521d8af..965e458 100644 --- a/client/src/main/java/info/frostfs/sdk/services/ApeManagerClient.java +++ b/client/src/main/java/info/frostfs/sdk/services/ApeManagerClient.java @@ -1,6 +1,6 @@ package info.frostfs.sdk.services; -import info.frostfs.sdk.dto.chain.Chain; +import frostfs.ape.Types; import info.frostfs.sdk.jdo.parameters.CallContext; import info.frostfs.sdk.jdo.parameters.ape.PrmApeChainAdd; import info.frostfs.sdk.jdo.parameters.ape.PrmApeChainList; @@ -13,5 +13,5 @@ public interface ApeManagerClient { void removeChain(PrmApeChainRemove args, CallContext ctx); - List listChains(PrmApeChainList args, CallContext ctx); + List listChains(PrmApeChainList args, CallContext ctx); } diff --git a/client/src/main/java/info/frostfs/sdk/services/impl/ApeManagerClientImpl.java b/client/src/main/java/info/frostfs/sdk/services/impl/ApeManagerClientImpl.java index 7843d65..cd81060 100644 --- a/client/src/main/java/info/frostfs/sdk/services/impl/ApeManagerClientImpl.java +++ b/client/src/main/java/info/frostfs/sdk/services/impl/ApeManagerClientImpl.java @@ -4,18 +4,17 @@ import com.google.protobuf.ByteString; import frostfs.ape.Types; import frostfs.apemanager.APEManagerServiceGrpc; import frostfs.apemanager.Service; -import info.frostfs.sdk.dto.chain.Chain; import info.frostfs.sdk.jdo.ClientEnvironment; import info.frostfs.sdk.jdo.parameters.CallContext; import info.frostfs.sdk.jdo.parameters.ape.PrmApeChainAdd; import info.frostfs.sdk.jdo.parameters.ape.PrmApeChainList; import info.frostfs.sdk.jdo.parameters.ape.PrmApeChainRemove; -import info.frostfs.sdk.mappers.chain.ChainMapper; import info.frostfs.sdk.mappers.chain.ChainTargetMapper; import info.frostfs.sdk.services.ApeManagerClient; import info.frostfs.sdk.services.ContextAccessor; import info.frostfs.sdk.tools.RequestConstructor; import info.frostfs.sdk.tools.RequestSigner; +import info.frostfs.sdk.tools.RuleSerializer; import info.frostfs.sdk.tools.Verifier; import java.util.List; @@ -58,7 +57,7 @@ public class ApeManagerClientImpl extends ContextAccessor implements ApeManagerC } @Override - public List listChains(PrmApeChainList args, CallContext ctx) { + public List listChains(PrmApeChainList args, CallContext ctx) { validate(args); var request = createListChainsRequest(args); @@ -68,12 +67,14 @@ public class ApeManagerClientImpl extends ContextAccessor implements ApeManagerC Verifier.checkResponse(response); - return ChainMapper.toModels(response.getBody().getChainsList()); + return response.getBody().getChainsList(); } private Service.AddChainRequest createAddChainRequest(PrmApeChainAdd args) { + var raw = RuleSerializer.serialize(args.getChain()); + var chainGrpc = Types.Chain.newBuilder() - .setRaw(ByteString.copyFrom(args.getChain().getRaw())) + .setRaw(ByteString.copyFrom(raw)) .build(); var body = Service.AddChainRequest.Body.newBuilder() .setChain(chainGrpc) @@ -90,7 +91,7 @@ public class ApeManagerClientImpl extends ContextAccessor implements ApeManagerC private Service.RemoveChainRequest createRemoveChainRequest(PrmApeChainRemove args) { var body = Service.RemoveChainRequest.Body.newBuilder() - .setChainId(ByteString.copyFrom(args.getChain().getRaw())) + .setChainId(ByteString.copyFrom(args.getChainId())) .setTarget(ChainTargetMapper.toGrpcMessage(args.getChainTarget())) .build(); var request = Service.RemoveChainRequest.newBuilder() diff --git a/client/src/main/java/info/frostfs/sdk/tools/MarshalFunction.java b/client/src/main/java/info/frostfs/sdk/tools/MarshalFunction.java new file mode 100644 index 0000000..accd7b1 --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/tools/MarshalFunction.java @@ -0,0 +1,5 @@ +package info.frostfs.sdk.tools; + +public interface MarshalFunction { + int marshal(byte[] buf, int offset, T t); +} diff --git a/client/src/main/java/info/frostfs/sdk/tools/RuleSerializer.java b/client/src/main/java/info/frostfs/sdk/tools/RuleSerializer.java new file mode 100644 index 0000000..097708d --- /dev/null +++ b/client/src/main/java/info/frostfs/sdk/tools/RuleSerializer.java @@ -0,0 +1,256 @@ +package info.frostfs.sdk.tools; + +import info.frostfs.sdk.dto.ape.*; +import info.frostfs.sdk.exceptions.ValidationFrostFSException; +import org.apache.commons.lang3.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.util.function.Function; + +import static info.frostfs.sdk.constants.ErrorConst.*; +import static info.frostfs.sdk.constants.RuleConst.*; +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +public class RuleSerializer { + private RuleSerializer() { + } + + public static byte[] serialize(Chain chain) { + int s = U_INT_8_SIZE // Marshaller version + + U_INT_8_SIZE // Chain version + + sliceSize(chain.getId(), b -> BYTE_SIZE) + + sliceSize(chain.getRules(), RuleSerializer::ruleSize) + + U_INT_8_SIZE; // MatchType + + byte[] buf = new byte[s]; + + int offset = uInt8Marshal(buf, 0, VERSION); + offset = uInt8Marshal(buf, offset, (byte) CHAIN_MARSHAL_VERSION); + offset = sliceMarshal(buf, offset, chain.getId(), RuleSerializer::byteMarshal); + offset = sliceMarshal(buf, offset, chain.getRules(), RuleSerializer::marshalRule); + offset = uInt8Marshal(buf, offset, (byte) chain.getMatchType().value); + + verifyMarshal(buf, offset); + + return buf; + } + + private static int sliceSize(T[] slice, Function sizeOf) { + if (isNull(slice)) { + return NULL_SLICE_SIZE; + } + + // Assuming int64Size is the size of the slice + int size = int64Size(slice.length); + for (T v : slice) { + size += sizeOf.apply(v); + } + + return size; + } + + /* + * https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=92;drc=dac9b9ddbd5160c5f4552410f5f828 + * 1bd5eed38c + * + * and + * + * https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=41;drc=dac9b9ddbd5160c5f4552410f5f828 + * 1bd5eed38c + * */ + private static int int64Size(long value) { + long ux = value << 1; + if (value < 0) { + ux = ~ux; + } + + int size = 0; + while (ux >= OFFSET128) { + size++; + ux >>>= UNSIGNED_SERIALIZE_SIZE; + } + + return size + 1; + } + + private static int stringSize(String s) { + int len = nonNull(s) ? s.length() : 0; + return int64Size(len) + len; + } + + private static int actionsSize(Actions action) { + return BOOL_SIZE // Inverted + + (nonNull(action) ? sliceSize(action.getNames(), RuleSerializer::stringSize) : 0); + } + + private static int resourcesSize(Resources resource) { + return BOOL_SIZE // Inverted + + (nonNull(resource) ? sliceSize(resource.getNames(), RuleSerializer::stringSize) : 0); + } + + private static int conditionSize(Condition condition) { + if (isNull(condition)) { + throw new ValidationFrostFSException(String.format(REQUIRED_FIELD_TEMPLATE, Rule.class.getName())); + } + + return BYTE_SIZE // Op + + BYTE_SIZE // Object + + stringSize(condition.getKey()) + + stringSize(condition.getValue()); + } + + private static int ruleSize(Rule rule) { + if (isNull(rule)) { + throw new ValidationFrostFSException(String.format(REQUIRED_FIELD_TEMPLATE, Rule.class.getName())); + } + + return BYTE_SIZE // Status + + actionsSize(rule.getActions()) + + resourcesSize(rule.getResources()) + + BOOL_SIZE // Any + + sliceSize(rule.getConditions(), RuleSerializer::conditionSize); + } + + private static int uInt8Marshal(byte[] buf, int offset, byte value) { + if (buf.length - offset < 1) { + throw new ValidationFrostFSException( + String.format(BYTES_ARE_OVER_FOR_SERIALIZE_TEMPLATE, Byte.class.getName(), 1) + ); + } + + buf[offset] = value; + + return offset + 1; + } + + private 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. + private static int putVarInt(byte[] buf, int offset, long x) { + long ux = x << 1; + if (x < 0) { + ux = ~ux; + } + + return putUVarInt(buf, offset, ux); + } + + private static int putUVarInt(byte[] buf, int offset, long x) { + while (x >= OFFSET128) { + buf[offset] = (byte) (x | OFFSET128); + x >>>= UNSIGNED_SERIALIZE_SIZE; + offset++; + } + buf[offset] = (byte) x; + return offset + 1; + } + + private static int int64Marshal(byte[] buf, int offset, long v) { + var size = int64Size(v); + if (buf.length - offset < size) { + throw new ValidationFrostFSException( + String.format(BYTES_ARE_OVER_FOR_SERIALIZE_TEMPLATE, Long.class.getName(), size) + ); + } + + return putVarInt(buf, offset, v); + } + + private static int sliceMarshal(byte[] buf, int offset, T[] slice, MarshalFunction marshalT) { + if (isNull(slice)) { + return int64Marshal(buf, offset, NULL_SLICE); + } + + if (slice.length > MAX_SLICE_LENGTH) { + throw new ValidationFrostFSException(String.format(SLICE_IS_TOO_BIG_TEMPLATE, slice.length)); + } + + offset = int64Marshal(buf, offset, slice.length); + for (T v : slice) { + offset = marshalT.marshal(buf, offset, v); + } + + return offset; + } + + private static int boolMarshal(byte[] buf, int offset, boolean value) { + return uInt8Marshal(buf, offset, value ? BYTE_TRUE : BYTE_FALSE); + } + + private static int stringMarshal(byte[] buf, int offset, String value) { + if (StringUtils.isBlank(value)) { + throw new ValidationFrostFSException(STRING_IS_BLANK); + } + + if (value.length() > MAX_SLICE_LENGTH) { + throw new ValidationFrostFSException(String.format(STRING_IS_TOO_BIG_TEMPLATE, value.length())); + } + + if (buf.length - offset < int64Size(value.length()) + value.length()) { + throw new ValidationFrostFSException( + String.format(BYTES_ARE_OVER_FOR_SERIALIZE_TEMPLATE, String.class.getName(), value.length()) + ); + } + + offset = int64Marshal(buf, offset, value.length()); + if (value.isEmpty()) { + return offset; + } + + byte[] stringBytes = value.getBytes(StandardCharsets.UTF_8); + + // Copy exactly value.length() bytes as in the original code + System.arraycopy(stringBytes, 0, buf, offset, value.length()); + return offset + value.length(); + } + + private static int marshalActions(byte[] buf, int offset, Actions action) { + if (isNull(action)) { + throw new ValidationFrostFSException(String.format(REQUIRED_FIELD_TEMPLATE, Actions.class.getName())); + } + + offset = boolMarshal(buf, offset, action.isInverted()); + return sliceMarshal(buf, offset, action.getNames(), RuleSerializer::stringMarshal); + } + + private static int marshalCondition(byte[] buf, int offset, Condition condition) { + if (isNull(condition)) { + throw new ValidationFrostFSException(String.format(REQUIRED_FIELD_TEMPLATE, Condition.class.getName())); + } + + offset = byteMarshal(buf, offset, (byte) condition.getOp().value); + offset = byteMarshal(buf, offset, (byte) condition.getKind().value); + offset = stringMarshal(buf, offset, condition.getKey()); + return stringMarshal(buf, offset, condition.getValue()); + } + + private static int marshalRule(byte[] buf, int offset, Rule rule) { + if (isNull(rule)) { + throw new ValidationFrostFSException(String.format(REQUIRED_FIELD_TEMPLATE, Rule.class.getName())); + } + + offset = byteMarshal(buf, offset, (byte) rule.getStatus().value); + offset = marshalActions(buf, offset, rule.getActions()); + offset = marshalResources(buf, offset, rule.getResources()); + offset = boolMarshal(buf, offset, rule.isAny()); + return sliceMarshal(buf, offset, rule.getConditions(), RuleSerializer::marshalCondition); + } + + private static int marshalResources(byte[] buf, int offset, Resources resources) { + if (isNull(resources)) { + throw new ValidationFrostFSException(String.format(REQUIRED_FIELD_TEMPLATE, Resources.class.getName())); + } + + offset = boolMarshal(buf, offset, resources.isInverted()); + return sliceMarshal(buf, offset, resources.getNames(), RuleSerializer::stringMarshal); + } + + private static void verifyMarshal(byte[] buf, int lastOffset) { + if (buf.length != lastOffset) { + throw new ValidationFrostFSException(MARSHAL_SIZE_DIFFERS); + } + } +} diff --git a/client/src/main/java/info/frostfs/sdk/tools/Verifier.java b/client/src/main/java/info/frostfs/sdk/tools/Verifier.java index f4dbde3..8a5e968 100644 --- a/client/src/main/java/info/frostfs/sdk/tools/Verifier.java +++ b/client/src/main/java/info/frostfs/sdk/tools/Verifier.java @@ -78,6 +78,10 @@ public class Verifier { } var metaHeader = (Types.ResponseMetaHeader) MessageHelper.getField(response, META_HEADER_FIELD_NAME); + if (isNull(metaHeader) || metaHeader.getSerializedSize() == 0) { + return; + } + var status = ResponseStatusMapper.toModel(metaHeader.getStatus()); if (!status.isSuccess()) { throw new ResponseFrostFSException(status); diff --git a/client/src/test/java/info/frostfs/sdk/services/ApeManagerClientTest.java b/client/src/test/java/info/frostfs/sdk/services/ApeManagerClientTest.java index fa09315..5e62c74 100644 --- a/client/src/test/java/info/frostfs/sdk/services/ApeManagerClientTest.java +++ b/client/src/test/java/info/frostfs/sdk/services/ApeManagerClientTest.java @@ -2,10 +2,9 @@ package info.frostfs.sdk.services; import frostfs.apemanager.APEManagerServiceGrpc; import frostfs.apemanager.Service; -import info.frostfs.sdk.FileUtils; -import info.frostfs.sdk.dto.chain.Chain; +import info.frostfs.sdk.dto.ape.*; import info.frostfs.sdk.dto.chain.ChainTarget; -import info.frostfs.sdk.enums.TargetType; +import info.frostfs.sdk.enums.*; import info.frostfs.sdk.exceptions.ValidationFrostFSException; import info.frostfs.sdk.jdo.ClientEnvironment; import info.frostfs.sdk.jdo.parameters.CallContext; @@ -18,6 +17,7 @@ import info.frostfs.sdk.tools.RequestConstructor; import info.frostfs.sdk.tools.RequestSigner; import info.frostfs.sdk.tools.Verifier; import io.grpc.Channel; +import org.apache.commons.lang3.ArrayUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,7 +30,8 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import java.lang.reflect.Field; -import java.util.stream.Collectors; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -39,6 +40,9 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ApeManagerClientTest { + private static final String CHAIN_BASE64 = + "AAAaY2hhaW4taWQtdGVzdAIAAAISR2V0T2JqZWN0AAIebmF0aXZlOm9iamVjdC8qAAIAABREZXBhcnRtZW50BEhSAA=="; + private ApeManagerClientImpl apeManagerClient; @Mock @@ -107,7 +111,9 @@ class ApeManagerClientTest { assertThat(result).containsOnly(response.getBody().getChainId().toByteArray()); var request = captor.getValue(); - assertThat(request.getBody().getChain().getRaw().toByteArray()).containsOnly(chain.getRaw()); + assertEquals( + Base64.getEncoder().encodeToString(request.getBody().getChain().getRaw().toByteArray()), CHAIN_BASE64) + ; assertEquals(chainTarget.getName(), request.getBody().getTarget().getName()); assertEquals(chainTarget.getType().value, request.getBody().getTarget().getType().getNumber()); } @@ -133,7 +139,7 @@ class ApeManagerClientTest { //Given Chain chain = generateChain(); ChainTarget chainTarget = generateChainTarget(); - PrmApeChainRemove params = new PrmApeChainRemove(chain, chainTarget); + PrmApeChainRemove params = new PrmApeChainRemove(Base64.getDecoder().decode(CHAIN_BASE64), chainTarget); var response = ApeManagerGenerator.generateRemoveChainResponse(); @@ -156,7 +162,7 @@ class ApeManagerClientTest { verifierMock.verify(() -> Verifier.checkResponse(response), times(1)); var request = captor.getValue(); - assertThat(request.getBody().getChainId().toByteArray()).containsOnly(chain.getRaw()); + assertThat(request.getBody().getChainId().toByteArray()).containsOnly(Base64.getDecoder().decode(CHAIN_BASE64)); assertEquals(chainTarget.getName(), request.getBody().getTarget().getName()); assertEquals(chainTarget.getType().value, request.getBody().getTarget().getType().getNumber()); } @@ -167,7 +173,7 @@ class ApeManagerClientTest { Chain chain = generateChain(); ChainTarget chainTarget = generateChainTarget(); PrmApeChainRemove params1 = new PrmApeChainRemove(null, chainTarget); - PrmApeChainRemove params2 = new PrmApeChainRemove(chain, null); + PrmApeChainRemove params2 = new PrmApeChainRemove(Base64.getDecoder().decode(CHAIN_BASE64), null); PrmApeChainRemove params3 = new PrmApeChainRemove(null, null); //When + Then @@ -202,11 +208,8 @@ class ApeManagerClientTest { ); verifierMock.verify(() -> Verifier.checkResponse(response), times(1)); - var actual = result.stream().map(Chain::getRaw).collect(Collectors.toList()); - var expected = response.getBody().getChainsList().stream() - .map(chain -> chain.getRaw().toByteArray()) - .collect(Collectors.toList()); - assertThat(actual).hasSize(10).containsAll(expected); + var expected = response.getBody().getChainsList(); + assertThat(result).hasSize(10).containsAll(expected); var request = captor.getValue(); assertEquals(chainTarget.getName(), request.getBody().getTarget().getName()); @@ -221,8 +224,24 @@ class ApeManagerClientTest { } private Chain generateChain() { - byte[] chainRaw = FileUtils.resourceToBytes("test_chain_raw.json"); - return new Chain(chainRaw); + var resources = new Resources(false, new String[]{"native:object/*"}); + var actions = new Actions(false, new String[]{"GetObject"}); + var condition = new Condition( + ConditionType.COND_STRING_EQUALS, ConditionKindType.RESOURCE, "Department", "HR" + ); + + var rule = new Rule(); + rule.setStatus(RuleStatus.ALLOW); + rule.setResources(resources); + rule.setActions(actions); + rule.setAny(false); + rule.setConditions(new Condition[]{condition}); + + var chain = new Chain(); + chain.setId(ArrayUtils.toObject("chain-id-test".getBytes(StandardCharsets.UTF_8))); + chain.setRules(new Rule[]{rule}); + chain.setMatchType(RuleMatchType.DENY_PRIORITY); + return chain; } private ChainTarget generateChainTarget() { diff --git a/exceptions/src/main/java/info/frostfs/sdk/constants/ErrorConst.java b/exceptions/src/main/java/info/frostfs/sdk/constants/ErrorConst.java index 62c3eb8..629943e 100644 --- a/exceptions/src/main/java/info/frostfs/sdk/constants/ErrorConst.java +++ b/exceptions/src/main/java/info/frostfs/sdk/constants/ErrorConst.java @@ -2,6 +2,7 @@ package info.frostfs.sdk.constants; public class ErrorConst { public static final String OBJECT_IS_NULL = "object must not be null"; + public static final String STRING_IS_BLANK = "string must not be blank"; public static final String INPUT_PARAM_IS_MISSING = "input parameter is not present"; public static final String SOME_PARAM_IS_MISSING = "one of the input parameters is not present"; public static final String PARAM_IS_MISSING_TEMPLATE = "param %s is not present"; @@ -51,6 +52,12 @@ public class ErrorConst { public static final String FIELDS_DELIMITER_COMMA = ", "; public static final String FIELDS_DELIMITER_OR = " or "; + public static final String MARSHAL_SIZE_DIFFERS = "actual data size differs from expected"; + public static final String BYTES_ARE_OVER_FOR_SERIALIZE_TEMPLATE = + "not enough bytes left to serialize value of type %s with length=%s"; + public static final String SLICE_IS_TOO_BIG_TEMPLATE = "slice size is too big=%s"; + public static final String STRING_IS_TOO_BIG_TEMPLATE = "string size is too big=%s"; + private ErrorConst() { } } diff --git a/models/src/main/java/info/frostfs/sdk/dto/ape/Actions.java b/models/src/main/java/info/frostfs/sdk/dto/ape/Actions.java new file mode 100644 index 0000000..83415a4 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/ape/Actions.java @@ -0,0 +1,15 @@ +package info.frostfs.sdk.dto.ape; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Actions { + private boolean inverted; + private String[] names; +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/ape/Chain.java b/models/src/main/java/info/frostfs/sdk/dto/ape/Chain.java new file mode 100644 index 0000000..0e8385f --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/ape/Chain.java @@ -0,0 +1,17 @@ +package info.frostfs.sdk.dto.ape; + +import info.frostfs.sdk.enums.RuleMatchType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Chain { + private Byte[] id; + private Rule[] rules; + private RuleMatchType matchType; +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/ape/Condition.java b/models/src/main/java/info/frostfs/sdk/dto/ape/Condition.java new file mode 100644 index 0000000..8334f94 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/ape/Condition.java @@ -0,0 +1,19 @@ +package info.frostfs.sdk.dto.ape; + +import info.frostfs.sdk.enums.ConditionKindType; +import info.frostfs.sdk.enums.ConditionType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Condition { + private ConditionType op; + private ConditionKindType kind; + private String key; + private String value; +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/ape/Resources.java b/models/src/main/java/info/frostfs/sdk/dto/ape/Resources.java new file mode 100644 index 0000000..017ed4a --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/ape/Resources.java @@ -0,0 +1,15 @@ +package info.frostfs.sdk.dto.ape; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Resources { + private boolean inverted; + private String[] names; +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/ape/Rule.java b/models/src/main/java/info/frostfs/sdk/dto/ape/Rule.java new file mode 100644 index 0000000..14e462f --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/dto/ape/Rule.java @@ -0,0 +1,27 @@ +package info.frostfs.sdk.dto.ape; + +import info.frostfs.sdk.enums.RuleStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Rule { + private RuleStatus status; + + // Actions the operation is applied to. + private Actions actions; + + // List of the resources the operation is applied to. + private Resources resources; + + // True if individual conditions must be combined with the logical OR. + // By default, AND is used, so _each_ condition must pass. + private boolean any; + + private Condition[] conditions; +} diff --git a/models/src/main/java/info/frostfs/sdk/dto/chain/Chain.java b/models/src/main/java/info/frostfs/sdk/dto/chain/Chain.java deleted file mode 100644 index 984b163..0000000 --- a/models/src/main/java/info/frostfs/sdk/dto/chain/Chain.java +++ /dev/null @@ -1,10 +0,0 @@ -package info.frostfs.sdk.dto.chain; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class Chain { - private final byte[] raw; -} diff --git a/models/src/main/java/info/frostfs/sdk/dto/response/ResponseStatus.java b/models/src/main/java/info/frostfs/sdk/dto/response/ResponseStatus.java index af69f98..02602f1 100644 --- a/models/src/main/java/info/frostfs/sdk/dto/response/ResponseStatus.java +++ b/models/src/main/java/info/frostfs/sdk/dto/response/ResponseStatus.java @@ -3,19 +3,21 @@ package info.frostfs.sdk.dto.response; import info.frostfs.sdk.enums.StatusCode; import lombok.Getter; import lombok.Setter; +import org.apache.commons.lang3.StringUtils; import static info.frostfs.sdk.constants.FieldConst.EMPTY_STRING; -import static java.util.Objects.isNull; @Getter @Setter public class ResponseStatus { private StatusCode code; private String message; + private String details; - public ResponseStatus(StatusCode code, String message) { + public ResponseStatus(StatusCode code, String message, String details) { this.code = code; - this.message = isNull(message) ? EMPTY_STRING : message; + this.message = StringUtils.isBlank(message) ? EMPTY_STRING : message; + this.details = StringUtils.isBlank(details) ? EMPTY_STRING : details; } public ResponseStatus(StatusCode code) { @@ -25,7 +27,7 @@ public class ResponseStatus { @Override public String toString() { - return String.format("Response status: %s. Message: %s.", code, message); + return String.format("Response status: %s. Message: %s. Details: %s", code, message, details); } public boolean isSuccess() { diff --git a/models/src/main/java/info/frostfs/sdk/enums/ConditionKindType.java b/models/src/main/java/info/frostfs/sdk/enums/ConditionKindType.java new file mode 100644 index 0000000..a437254 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/enums/ConditionKindType.java @@ -0,0 +1,13 @@ +package info.frostfs.sdk.enums; + +public enum ConditionKindType { + RESOURCE(0), + REQUEST(1), + ; + + public final int value; + + ConditionKindType(int value) { + this.value = value; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/enums/ConditionType.java b/models/src/main/java/info/frostfs/sdk/enums/ConditionType.java new file mode 100644 index 0000000..b23417c --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/enums/ConditionType.java @@ -0,0 +1,36 @@ +package info.frostfs.sdk.enums; + +public enum ConditionType { + COND_STRING_EQUALS(0), + COND_STRING_NOT_EQUALS(1), + COND_STRING_EQUALS_IGNORE_CASE(2), + COND_STRING_NOT_EQUALS_IGNORE_CASE(3), + + COND_STRING_LIKE(4), + COND_STRING_NOT_LIKE(5), + + COND_STRING_LESS_THAN(6), + COND_STRING_LESS_THAN_EQUALS(7), + COND_STRING_GREATER_THAN(8), + COND_STRING_GREATER_THAN_EQUALS(9), + + COND_NUMERIC_EQUALS(10), + COND_NUMERIC_NOT_EQUALS(11), + + COND_NUMERIC_LESS_THAN(12), + COND_NUMERIC_LESS_THAN_EQUALS(13), + COND_NUMERIC_GREATER_THAN(14), + COND_NUMERIC_GREATER_THAN_EQUALS(15), + + COND_SLICE_CONTAINS(16), + + COND_IP_ADDRESS(17), + COND_NOT_IP_ADDRESS(18), + ; + + public final int value; + + ConditionType(int value) { + this.value = value; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/enums/RuleMatchType.java b/models/src/main/java/info/frostfs/sdk/enums/RuleMatchType.java new file mode 100644 index 0000000..bfacaf6 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/enums/RuleMatchType.java @@ -0,0 +1,16 @@ +package info.frostfs.sdk.enums; + +public enum RuleMatchType { + // DENY_PRIORITY rejects the request if any `Deny` is specified. + DENY_PRIORITY(0), + + // FIRST_MATCH returns the first rule action matched to the request. + FIRST_MATCH(1), + ; + + public final int value; + + RuleMatchType(int value) { + this.value = value; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/enums/RuleStatus.java b/models/src/main/java/info/frostfs/sdk/enums/RuleStatus.java new file mode 100644 index 0000000..b2f6cf1 --- /dev/null +++ b/models/src/main/java/info/frostfs/sdk/enums/RuleStatus.java @@ -0,0 +1,15 @@ +package info.frostfs.sdk.enums; + +public enum RuleStatus { + ALLOW(0), + NO_RULE_FOUND(1), + ACCESS_DENIED(2), + QUOTA_LIMIT_REACHED(3), + ; + + public final int value; + + RuleStatus(int value) { + this.value = value; + } +} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/chain/ChainMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/chain/ChainMapper.java deleted file mode 100644 index 20197f7..0000000 --- a/models/src/main/java/info/frostfs/sdk/mappers/chain/ChainMapper.java +++ /dev/null @@ -1,31 +0,0 @@ -package info.frostfs.sdk.mappers.chain; - -import frostfs.ape.Types; -import info.frostfs.sdk.dto.chain.Chain; -import org.apache.commons.collections4.CollectionUtils; - -import java.util.List; -import java.util.stream.Collectors; - -import static java.util.Objects.isNull; - -public class ChainMapper { - private ChainMapper() { - } - - public static List toModels(List chains) { - if (CollectionUtils.isEmpty(chains)) { - return null; - } - - return chains.stream().map(ChainMapper::toModel).collect(Collectors.toList()); - } - - public static Chain toModel(Types.Chain chain) { - if (isNull(chain) || chain.getSerializedSize() == 0) { - return null; - } - - return new Chain(chain.getRaw().toByteArray()); - } -} diff --git a/models/src/main/java/info/frostfs/sdk/mappers/response/ResponseStatusMapper.java b/models/src/main/java/info/frostfs/sdk/mappers/response/ResponseStatusMapper.java index cb935dd..59c1563 100644 --- a/models/src/main/java/info/frostfs/sdk/mappers/response/ResponseStatusMapper.java +++ b/models/src/main/java/info/frostfs/sdk/mappers/response/ResponseStatusMapper.java @@ -5,6 +5,9 @@ import info.frostfs.sdk.dto.response.ResponseStatus; import info.frostfs.sdk.enums.StatusCode; import info.frostfs.sdk.exceptions.ProcessFrostFSException; +import java.util.stream.Collectors; + +import static info.frostfs.sdk.constants.ErrorConst.FIELDS_DELIMITER_COMMA; import static info.frostfs.sdk.constants.ErrorConst.UNKNOWN_ENUM_VALUE_TEMPLATE; import static java.util.Objects.isNull; @@ -24,6 +27,10 @@ public class ResponseStatusMapper { ); } - return new ResponseStatus(statusCode, status.getMessage()); + var stringDetails = status.getDetailsList().stream() + .map(t -> t.getValue().toStringUtf8()) + .collect(Collectors.toList()); + + return new ResponseStatus(statusCode, status.getMessage(), String.join(FIELDS_DELIMITER_COMMA, stringDetails)); } } diff --git a/models/src/test/java/info/frostfs/sdk/mappers/chain/ChainMapperTest.java b/models/src/test/java/info/frostfs/sdk/mappers/chain/ChainMapperTest.java deleted file mode 100644 index 7d2c4b6..0000000 --- a/models/src/test/java/info/frostfs/sdk/mappers/chain/ChainMapperTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package info.frostfs.sdk.mappers.chain; - -import com.google.protobuf.ByteString; -import frostfs.ape.Types; -import org.junit.jupiter.api.Test; - -import java.util.Collections; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -public class ChainMapperTest { - - @Test - void toModels_success() { - //Given - var chain1 = Types.Chain.newBuilder() - .setRaw(ByteString.copyFrom(new byte[]{1, 2, 3, 4, 5})) - .build(); - var chain2 = Types.Chain.newBuilder() - .setRaw(ByteString.copyFrom(new byte[]{6, 7, 8, 9, 10})) - .build(); - - //When - var result = ChainMapper.toModels(List.of(chain1, chain2)); - - //Then - assertNotNull(result); - assertThat(result).hasSize(2); - assertThat(result.get(0).getRaw()).containsOnly(chain1.getRaw().toByteArray()); - assertThat(result.get(1).getRaw()).containsOnly(chain2.getRaw().toByteArray()); - } - - @Test - void toModels_null() { - //When + Then - assertNull(ChainMapper.toModels(null)); - assertNull(ChainMapper.toModels(Collections.emptyList())); - } - - @Test - void toModel_success() { - //Given - var chain = Types.Chain.newBuilder() - .setRaw(ByteString.copyFrom(new byte[]{1, 2, 3, 4, 5})) - .build(); - - //When - var result = ChainMapper.toModel(chain); - - //Then - assertNotNull(result); - assertThat(result.getRaw()).containsOnly(chain.getRaw().toByteArray()); - } - - @Test - void toModel_null() { - //When + Then - assertNull(ChainMapper.toModel(null)); - assertNull(ChainMapper.toModel(Types.Chain.getDefaultInstance())); - } -} diff --git a/pom.xml b/pom.xml index 313d679..2f42332 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ - 0.5.0 + 0.6.0 11 11