Add a require.proto presence annotation
This commit is contained in:
parent
cea2abcf6e
commit
afa1899dc9
|
@ -21,6 +21,7 @@ import org.whispersystems.textsecuregcm.grpc.validators.EnumSpecifiedFieldValida
|
|||
import org.whispersystems.textsecuregcm.grpc.validators.ExactlySizeFieldValidator;
|
||||
import org.whispersystems.textsecuregcm.grpc.validators.FieldValidator;
|
||||
import org.whispersystems.textsecuregcm.grpc.validators.NonEmptyFieldValidator;
|
||||
import org.whispersystems.textsecuregcm.grpc.validators.PresentFieldValidator;
|
||||
import org.whispersystems.textsecuregcm.grpc.validators.RangeFieldValidator;
|
||||
import org.whispersystems.textsecuregcm.grpc.validators.SizeFieldValidator;
|
||||
|
||||
|
@ -28,6 +29,7 @@ public class ValidatingInterceptor implements ServerInterceptor {
|
|||
|
||||
private final Map<String, FieldValidator> fieldValidators = Map.of(
|
||||
"org.signal.chat.require.nonEmpty", new NonEmptyFieldValidator(),
|
||||
"org.signal.chat.require.present", new PresentFieldValidator(),
|
||||
"org.signal.chat.require.specified", new EnumSpecifiedFieldValidator(),
|
||||
"org.signal.chat.require.e164", new E164FieldValidator(),
|
||||
"org.signal.chat.require.exactlySize", new ExactlySizeFieldValidator(),
|
||||
|
|
|
@ -12,6 +12,7 @@ import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.in
|
|||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.Descriptors;
|
||||
import com.google.protobuf.GeneratedMessageV3;
|
||||
import com.google.protobuf.Message;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusException;
|
||||
import java.util.Set;
|
||||
|
@ -91,7 +92,10 @@ public abstract class BaseFieldValidator<T> implements FieldValidator {
|
|||
validateBytesValue(extensionValueTyped, (ByteString) msg.getField(fd));
|
||||
case ENUM ->
|
||||
validateEnumValue(extensionValueTyped, (Descriptors.EnumValueDescriptor) msg.getField(fd));
|
||||
case FLOAT, DOUBLE, BOOL, MESSAGE, GROUP -> {
|
||||
case MESSAGE -> {
|
||||
validateMessageValue(extensionValueTyped, (Message) msg.getField(fd));
|
||||
}
|
||||
case FLOAT, DOUBLE, BOOL, GROUP -> {
|
||||
// at this moment, there are no validations specific to these types of fields
|
||||
}
|
||||
}
|
||||
|
@ -140,6 +144,12 @@ public abstract class BaseFieldValidator<T> implements FieldValidator {
|
|||
throw internalError("`validateEnumValue` method needs to be implemented");
|
||||
}
|
||||
|
||||
protected void validateMessageValue(
|
||||
final T extensionValue,
|
||||
final Message message) throws StatusException {
|
||||
throw internalError("`validateMessageValue` method needs to be implemented");
|
||||
}
|
||||
|
||||
protected static boolean requireFlagExtension(final Object extensionValue) throws StatusException {
|
||||
if (extensionValue instanceof Boolean flagIsOn && flagIsOn) {
|
||||
return true;
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc.validators;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.grpc.validators.ValidatorUtils.invalidArgument;
|
||||
|
||||
import com.google.protobuf.Descriptors;
|
||||
import com.google.protobuf.Message;
|
||||
import io.grpc.StatusException;
|
||||
import java.util.Set;
|
||||
|
||||
public class PresentFieldValidator extends BaseFieldValidator<Boolean> {
|
||||
|
||||
public PresentFieldValidator() {
|
||||
super("present",
|
||||
Set.of(Descriptors.FieldDescriptor.Type.MESSAGE),
|
||||
MissingOptionalAction.FAIL,
|
||||
true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean resolveExtensionValue(final Object extensionValue) throws StatusException {
|
||||
return requireFlagExtension(extensionValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void validateMessageValue(final Boolean extensionValue, final Message msg) throws StatusException {
|
||||
if (msg == null) {
|
||||
throw invalidArgument("message expected to be present");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,123 +13,134 @@ import "google/protobuf/descriptor.proto";
|
|||
|
||||
extend google.protobuf.FieldOptions {
|
||||
/*
|
||||
Requires a field to have content of non-zero size/length.
|
||||
Applies to both `optional` and regular fields, i.e. if the field is not set or has a default value,
|
||||
it's considered to be empty.
|
||||
|
||||
```
|
||||
import "org/signal/chat/require.proto";
|
||||
|
||||
message Data {
|
||||
|
||||
string nonEmptyString = 1 [(require.nonEmpty) = true];
|
||||
|
||||
bytes nonEmptyBytes = 2 [(require.nonEmpty) = true];
|
||||
|
||||
optional string nonEmptyStringOptional = 3 [(require.nonEmpty) = true];
|
||||
|
||||
optional bytes nonEmptyBytesOptional = 4 [(require.nonEmpty) = true];
|
||||
|
||||
repeated string nonEmptyList = 5 [(require.nonEmpty) = true];
|
||||
}
|
||||
```
|
||||
|
||||
Applicable to fields of type `string`, `byte`, and `repeated` fields.
|
||||
* Requires a field to have content of non-zero size/length.
|
||||
* Applies to both `optional` and regular fields, i.e. if the field is not set or has a default value,
|
||||
* it's considered to be empty.
|
||||
*
|
||||
* ```
|
||||
* import "org/signal/chat/require.proto";
|
||||
*
|
||||
* message Data {
|
||||
* string nonEmptyString = 1 [(require.nonEmpty) = true];
|
||||
* bytes nonEmptyBytes = 2 [(require.nonEmpty) = true];
|
||||
* optional string nonEmptyStringOptional = 3 [(require.nonEmpty) = true];
|
||||
* optional bytes nonEmptyBytesOptional = 4 [(require.nonEmpty) = true];
|
||||
* repeated string nonEmptyList = 5 [(require.nonEmpty) = true];
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Applicable to fields of type `string`, `byte`, and `repeated` fields.
|
||||
*/
|
||||
optional bool nonEmpty = 70001;
|
||||
|
||||
/*
|
||||
Requires a enum field to have value with an index greater than zero.
|
||||
Applies to both `optional` and regular fields, i.e. if the field is not set or has a default value,
|
||||
its index will be <= 0.
|
||||
|
||||
```
|
||||
import "org/signal/chat/require.proto";
|
||||
|
||||
message Data {
|
||||
Color color = 1 [(require.specified) = true];
|
||||
}
|
||||
|
||||
enum Color {
|
||||
COLOR_UNSPECIFIED = 0;
|
||||
COLOR_RED = 1;
|
||||
COLOR_GREEN = 2;
|
||||
COLOR_BLUE = 3;
|
||||
}
|
||||
```
|
||||
* Requires a enum field to have value with an index greater than zero.
|
||||
* Applies to both `optional` and regular fields, i.e. if the field is not set or has a default value,
|
||||
* its index will be <= 0.
|
||||
*
|
||||
* ```
|
||||
* import "org/signal/chat/require.proto";
|
||||
*
|
||||
* message Data {
|
||||
* Color color = 1 [(require.specified) = true];
|
||||
* }
|
||||
*
|
||||
* enum Color {
|
||||
* COLOR_UNSPECIFIED = 0;
|
||||
* COLOR_RED = 1;
|
||||
* COLOR_GREEN = 2;
|
||||
* COLOR_BLUE = 3;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
optional bool specified = 70002;
|
||||
|
||||
/*
|
||||
Requires a size/length of a field to be within certain boundaries.
|
||||
Applies to both `optional` and regular fields, i.e. if the field is not set or has a default value,
|
||||
its size considered to be zero.
|
||||
|
||||
```
|
||||
import "org/signal/chat/require.proto";
|
||||
|
||||
message Data {
|
||||
|
||||
string name = 1 [(require.size) = {min: 3, max: 8}];
|
||||
|
||||
optional string address = 2 [(require.size) = {min: 3, max: 8}];
|
||||
}
|
||||
```
|
||||
|
||||
Applicable to fields of type `string`, `byte`, and `repeated` fields.
|
||||
* Requires a size/length of a field to be within certain boundaries.
|
||||
* Applies to both `optional` and regular fields, i.e. if the field is not set or has a default value,
|
||||
* its size considered to be zero.
|
||||
*
|
||||
* ```
|
||||
* import "org/signal/chat/require.proto";
|
||||
*
|
||||
* message Data {
|
||||
*
|
||||
* string name = 1 [(require.size) = {min: 3, max: 8}];
|
||||
*
|
||||
* optional string address = 2 [(require.size) = {min: 3, max: 8}];
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Applicable to fields of type `string`, `byte`, and `repeated` fields.
|
||||
*/
|
||||
optional SizeConstraint size = 70003;
|
||||
|
||||
/*
|
||||
Requires a size/length of a field to be one of the specified values.
|
||||
Applies to both `optional` and regular fields, i.e. if the field is not set or has a default value,
|
||||
its size considered to be zero.
|
||||
|
||||
```
|
||||
import "org/signal/chat/require.proto";
|
||||
|
||||
message Data {
|
||||
|
||||
string zip = 1 [(require.exactlySize) = 5];
|
||||
|
||||
optional string exactlySizeVariants = 2 [(require.exactlySize) = 2, (require.exactlySize) = 4];
|
||||
}
|
||||
```
|
||||
|
||||
Applicable to fields of type `string`, `byte`, and `repeated` fields.
|
||||
* Requires a size/length of a field to be one of the specified values.
|
||||
* Applies to both `optional` and regular fields, i.e. if the field is not set or has a default value,
|
||||
* its size considered to be zero.
|
||||
*
|
||||
* ```
|
||||
* import "org/signal/chat/require.proto";
|
||||
*
|
||||
* message Data {
|
||||
*
|
||||
* string zip = 1 [(require.exactlySize) = 5];
|
||||
*
|
||||
* optional string exactlySizeVariants = 2 [(require.exactlySize) = 2, (require.exactlySize) = 4];
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Applicable to fields of type `string`, `byte`, and `repeated` fields.
|
||||
*/
|
||||
repeated uint32 exactlySize = 70004;
|
||||
|
||||
/*
|
||||
Requires a value of a string field to be a valid E164-normalized phone number.
|
||||
If the field is `optional`, this check allows a value to be not set.
|
||||
|
||||
```
|
||||
import "org/signal/chat/require.proto";
|
||||
|
||||
message Data {
|
||||
string number = 1 [(require.e164)];
|
||||
}
|
||||
```
|
||||
* Requires a value of a string field to be a valid E164-normalized phone number.
|
||||
* If the field is `optional`, this check allows a value to be not set.
|
||||
*
|
||||
* ```
|
||||
* import "org/signal/chat/require.proto";
|
||||
*
|
||||
* message Data {
|
||||
* string number = 1 [(require.e164)];
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
optional bool e164 = 70005;
|
||||
|
||||
/*
|
||||
Requires an integer value to be within a certain range. The range boundaries are specified
|
||||
with the values of type `int32`, which should be enough for all practical purposes.
|
||||
|
||||
If the field is `optional`, this check allows a value to be not set.
|
||||
|
||||
```
|
||||
import "org/signal/chat/require.proto";
|
||||
|
||||
message Data {
|
||||
int32 byte = 1 [(require.range) = {min = -128, max = 127}];
|
||||
uint32 unsignedByte = 2 [(require.range).max = 255];
|
||||
}
|
||||
```
|
||||
* Requires an integer value to be within a certain range. The range boundaries are specified
|
||||
* with the values of type `int32`, which should be enough for all practical purposes.
|
||||
*
|
||||
* If the field is `optional`, this check allows a value to be not set.
|
||||
*
|
||||
* ```
|
||||
* import "org/signal/chat/require.proto";
|
||||
*
|
||||
* message Data {
|
||||
* int32 byte = 1 [(require.range) = {min: -128, max: 127}];
|
||||
* uint32 unsignedByte = 2 [(require.range).max = 255];
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
optional ValueRangeConstraint range = 70006;
|
||||
|
||||
/*
|
||||
* Require a value of a message field to be present.
|
||||
*
|
||||
* Applies to both `optional` and regular fields (both of which have explicit
|
||||
* presence for the message type anyways)
|
||||
*
|
||||
* ```
|
||||
* import "org/signal/chat/require.proto";
|
||||
* message Data {
|
||||
* message MyMessage {}
|
||||
* MyMessage myMessage = 1 [(require.present) = true];
|
||||
* }
|
||||
*````
|
||||
*/
|
||||
optional bool present = 70007;
|
||||
}
|
||||
|
||||
message SizeConstraint {
|
||||
|
@ -144,17 +155,17 @@ message ValueRangeConstraint {
|
|||
|
||||
extend google.protobuf.ServiceOptions {
|
||||
/*
|
||||
Indicates that all methods in a given service require a certain kind of authentication.
|
||||
|
||||
```
|
||||
import "org/signal/chat/require.proto";
|
||||
|
||||
service AuthService {
|
||||
option (require.auth) = AUTH_ONLY_AUTHENTICATED;
|
||||
|
||||
rpc AuthenticatedMethod (google.protobuf.Empty) returns (google.protobuf.Empty) {}
|
||||
}
|
||||
```
|
||||
* Indicates that all methods in a given service require a certain kind of authentication.
|
||||
*
|
||||
* ```
|
||||
* import "org/signal/chat/require.proto";
|
||||
*
|
||||
* service AuthService {
|
||||
* option (require.auth) = AUTH_ONLY_AUTHENTICATED;
|
||||
*
|
||||
* rpc AuthenticatedMethod (google.protobuf.Empty) returns (google.protobuf.Empty) {}
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
optional Auth auth = 71001;
|
||||
}
|
||||
|
|
|
@ -47,7 +47,9 @@ public class ValidatingInterceptorTest {
|
|||
@RegisterExtension
|
||||
static final GrpcServerExtension GRPC_SERVER_EXTENSION = new GrpcServerExtension();
|
||||
|
||||
private static final class ValidationTestGrpcServiceImpl extends ReactorValidationTestServiceGrpc.ValidationTestServiceImplBase {
|
||||
private static final class ValidationTestGrpcServiceImpl extends
|
||||
ReactorValidationTestServiceGrpc.ValidationTestServiceImplBase {
|
||||
|
||||
@Override
|
||||
public Mono<ValidationsResponse> validationsEndpoint(final ValidationsRequest request) {
|
||||
return Mono.just(ValidationsResponse.newBuilder().build());
|
||||
|
@ -55,6 +57,7 @@ public class ValidatingInterceptorTest {
|
|||
}
|
||||
|
||||
private static final class AuthGrpcServiceImpl extends ReactorAuthServiceGrpc.AuthServiceImplBase {
|
||||
|
||||
@Override
|
||||
public Mono<Empty> authenticatedMethod(final Empty request) {
|
||||
return Mono.just(Empty.getDefaultInstance());
|
||||
|
@ -62,6 +65,7 @@ public class ValidatingInterceptorTest {
|
|||
}
|
||||
|
||||
private static final class AnonymousGrpcServiceImpl extends ReactorAnonymousServiceGrpc.AnonymousServiceImplBase {
|
||||
|
||||
@Override
|
||||
public Mono<Empty> anonymousMethod(final Empty request) {
|
||||
return Mono.just(Empty.getDefaultInstance());
|
||||
|
@ -350,6 +354,21 @@ public class ValidatingInterceptorTest {
|
|||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPresent() throws Exception {
|
||||
assertStatusException(Status.INVALID_ARGUMENT, () -> stub.validationsEndpoint(
|
||||
builderWithValidDefaults()
|
||||
.clearPresentMessage()
|
||||
.build()
|
||||
));
|
||||
|
||||
assertStatusException(Status.INVALID_ARGUMENT, () -> stub.validationsEndpoint(
|
||||
builderWithValidDefaults()
|
||||
.clearOptionalPresentMessage()
|
||||
.build()
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllFieldsValidationSuccess() throws Exception {
|
||||
stub.validationsEndpoint(builderWithValidDefaults().build());
|
||||
|
@ -369,6 +388,8 @@ public class ValidatingInterceptorTest {
|
|||
.setRangeSizeString("abc")
|
||||
.setNonEmptyString("abc")
|
||||
.setNonEmptyStringOptional("abc")
|
||||
.setPresentMessage(ValidationsRequest.RequirePresentMessage.getDefaultInstance())
|
||||
.setOptionalPresentMessage(ValidationsRequest.RequirePresentMessage.getDefaultInstance())
|
||||
.setColor(Color.COLOR_GREEN)
|
||||
.setColorOptional(Color.COLOR_GREEN)
|
||||
.setNonEmptyBytes(ByteString.copyFrom(new byte[5]))
|
||||
|
|
|
@ -62,6 +62,10 @@ message ValidationsRequest {
|
|||
uint32 ui32 = 22 [(require.range).max = 100];
|
||||
int32 i32range = 23 [(require.range) = {min: 10, max: 20}];
|
||||
optional int32 i32OptRange = 24 [(require.range) = {min: 10, max: 20}];
|
||||
|
||||
message RequirePresentMessage {}
|
||||
RequirePresentMessage presentMessage = 25 [(require.present) = true];
|
||||
optional RequirePresentMessage optionalPresentMessage = 26 [(require.present) = true];
|
||||
}
|
||||
|
||||
message MessageWithInvalidRangeConstraint {
|
||||
|
|
Loading…
Reference in New Issue