diff --git a/api/src/main/java/com/oviva/spicegen/api/CheckPermission.java b/api/src/main/java/com/oviva/spicegen/api/CheckPermission.java new file mode 100644 index 0000000..321e474 --- /dev/null +++ b/api/src/main/java/com/oviva/spicegen/api/CheckPermission.java @@ -0,0 +1,30 @@ +package com.oviva.spicegen.api; + +import com.oviva.spicegen.api.internal.CheckPermissionImpl; + +public interface CheckPermission { + + static Builder newBuilder() { + return new CheckPermissionImpl.Builder(); + } + + ObjectRef resource(); + + String permission(); + + SubjectRef subject(); + + Consistency consistency(); + + interface Builder { + Builder resource(ObjectRef resource); + + Builder permission(String permission); + + Builder subject(SubjectRef subject); + + Builder consistency(Consistency consistency); + + CheckPermission build(); + } +} diff --git a/api/src/main/java/com/oviva/spicegen/api/Consistency.java b/api/src/main/java/com/oviva/spicegen/api/Consistency.java new file mode 100644 index 0000000..e642910 --- /dev/null +++ b/api/src/main/java/com/oviva/spicegen/api/Consistency.java @@ -0,0 +1,26 @@ +package com.oviva.spicegen.api; + +import com.oviva.spicegen.api.internal.ConsistencyImpl; + +public interface Consistency { + + String consistencyToken(); + + Requirement requirement(); + + static Consistency fullyConsistent() { + return ConsistencyImpl.fullyConsistent(); + } + + static Consistency atLeastAsFreshAs(String consistencyToken) { + if (consistencyToken == null) { + return ConsistencyImpl.fullyConsistent(); + } + return new ConsistencyImpl(Consistency.Requirement.AT_LEAST_AS_FRESH, consistencyToken); + } + + enum Requirement { + FULLY_CONSISTENT, + AT_LEAST_AS_FRESH + } +} diff --git a/api/src/main/java/com/oviva/spicegen/api/ObjectRef.java b/api/src/main/java/com/oviva/spicegen/api/ObjectRef.java index d6f1fcc..42d7d39 100644 --- a/api/src/main/java/com/oviva/spicegen/api/ObjectRef.java +++ b/api/src/main/java/com/oviva/spicegen/api/ObjectRef.java @@ -1,21 +1,20 @@ package com.oviva.spicegen.api; +import com.oviva.spicegen.api.internal.ObjectRefImpl; + public interface ObjectRef { String kind(); String id(); static ObjectRef of(String kind, String id) { - return new ObjectRef() { - @Override - public String kind() { - return kind; - } + if (kind == null) { + throw new IllegalArgumentException("kind must not be null"); + } + if (id == null) { + throw new IllegalArgumentException("id must not be null"); + } - @Override - public String id() { - return id; - } - }; + return new ObjectRefImpl(kind, id); } } diff --git a/api/src/main/java/com/oviva/spicegen/api/PermissionService.java b/api/src/main/java/com/oviva/spicegen/api/PermissionService.java index 703f354..96e15bf 100644 --- a/api/src/main/java/com/oviva/spicegen/api/PermissionService.java +++ b/api/src/main/java/com/oviva/spicegen/api/PermissionService.java @@ -12,4 +12,12 @@ public interface PermissionService { * @return the result, containing the consistencyToken */ UpdateResult updateRelationships(UpdateRelationships updateRelationships); + + /** + * Checks whether a subject has the given permission on a resource + * + * @param checkPermission the request + * @return true it the subject is permitted, false otherwise + */ + boolean checkPermission(CheckPermission checkPermission); } diff --git a/api/src/main/java/com/oviva/spicegen/api/Precondition.java b/api/src/main/java/com/oviva/spicegen/api/Precondition.java index f530d4c..5004129 100644 --- a/api/src/main/java/com/oviva/spicegen/api/Precondition.java +++ b/api/src/main/java/com/oviva/spicegen/api/Precondition.java @@ -1,63 +1,36 @@ package com.oviva.spicegen.api; -public final class Precondition { +import com.oviva.spicegen.api.internal.PreconditionImpl; - private final Condition condition; - private final RelationshipFilter filter; +public interface Precondition { - private Precondition(Builder builder) { - condition = builder.condition; - filter = builder.filter; - } + Condition condition(); - public static Precondition mustMatch(RelationshipFilter filter) { - return new Precondition(Condition.MUST_MATCH, filter); - } + RelationshipFilter filter(); - public static Precondition mustNotMatch(RelationshipFilter filter) { - return new Precondition(Condition.MUST_NOT_MATCH, filter); + static Precondition mustMatch(RelationshipFilter filter) { + return new PreconditionImpl(Condition.MUST_MATCH, filter); } - public static Builder newBuilder() { - return new Builder(); + static Precondition mustNotMatch(RelationshipFilter filter) { + return new PreconditionImpl(Condition.MUST_NOT_MATCH, filter); } - private Precondition(Condition condition, RelationshipFilter filter) { - this.condition = condition; - this.filter = filter; - } - - public Condition condition() { - return condition; - } - - public RelationshipFilter filter() { - return filter; - } - - public enum Condition { + enum Condition { MUST_MATCH, MUST_NOT_MATCH } - public static final class Builder { - private Condition condition; - private RelationshipFilter filter; + static Builder newBuilder() { + return new PreconditionImpl.Builder(); + } - private Builder() {} + interface Builder { - public Builder withCondition(Condition val) { - condition = val; - return this; - } + Precondition.Builder condition(Condition condition); - public Builder withFilter(RelationshipFilter val) { - filter = val; - return this; - } + Precondition.Builder filter(RelationshipFilter filter); - public Precondition build() { - return new Precondition(this); - } + Precondition build(); } } diff --git a/api/src/main/java/com/oviva/spicegen/api/RelationshipFilter.java b/api/src/main/java/com/oviva/spicegen/api/RelationshipFilter.java index ba564d7..8ec17ab 100644 --- a/api/src/main/java/com/oviva/spicegen/api/RelationshipFilter.java +++ b/api/src/main/java/com/oviva/spicegen/api/RelationshipFilter.java @@ -1,137 +1,56 @@ package com.oviva.spicegen.api; +import com.oviva.spicegen.api.internal.RelationshipFilterImpl; import java.util.Optional; -public final class RelationshipFilter { +public interface RelationshipFilter { - private final String resourceKind; - - /** optional */ - private final String resourceId; - - /** optional */ - private final String relation; - - /** optional */ - private final SubjectFilter subjectFilter; - - private RelationshipFilter(Builder builder) { - resourceKind = builder.resourceKind; - resourceId = builder.resourceId; - relation = builder.relation; - subjectFilter = builder.subjectFilter; + static Builder newBuilder() { + return new RelationshipFilterImpl.Builder(); } - public static Builder newBuilder() { - return new Builder(); - } - - public String resourceKind() { - return resourceKind; - } - - public Optional resourceId() { - return Optional.ofNullable(resourceId); - } - - public Optional relation() { - return Optional.ofNullable(relation); - } - - public Optional subjectFilter() { - return Optional.ofNullable(subjectFilter); - } + String resourceKind(); - public static class SubjectFilter { + Optional resourceId(); - private final String subjectKind; + Optional relation(); - /** optional */ - private final String subjectId; + Optional subjectFilter(); - /** optional */ - private final String relation; + interface SubjectFilter { - private SubjectFilter(Builder builder) { - subjectKind = builder.subjectKind; - subjectId = builder.subjectId; - relation = builder.relation; + static Builder newBuilder() { + return new RelationshipFilterImpl.SubjectFilterImpl.Builder(); } - public static Builder newBuilder() { - return new Builder(); - } - - public String subjectKind() { - return subjectKind; - } + String subjectKind(); - public Optional subjectId() { - return Optional.ofNullable(subjectId); - } - - public Optional relation() { - return Optional.ofNullable(relation); - } + Optional subjectId(); - public static final class Builder { - private String subjectKind; - private String subjectId; - private String relation; + Optional relation(); - private Builder() {} + interface Builder { - public Builder withSubjectKind(String val) { - subjectKind = val; - return this; - } + Builder subjectKind(String val); - public Builder withSubjectId(String val) { - subjectId = val; - return this; - } + Builder subjectId(String val); - public Builder withRelation(String val) { - relation = val; - return this; - } + Builder relation(String val); - public SubjectFilter build() { - return new SubjectFilter(this); - } + SubjectFilter build(); } } - public static final class Builder { - private String resourceKind; - private String resourceId; - private String relation; - private SubjectFilter subjectFilter; + interface Builder { - private Builder() {} + Builder resourceKind(String val); - public Builder withResourceKind(String val) { - resourceKind = val; - return this; - } - - public Builder withResourceId(String val) { - resourceId = val; - return this; - } + Builder resourceId(String val); - public Builder withRelation(String val) { - relation = val; - return this; - } + Builder relation(String val); - public Builder withSubjectFilter(SubjectFilter val) { - subjectFilter = val; - return this; - } + Builder subjectFilter(SubjectFilter val); - public RelationshipFilter build() { - return new RelationshipFilter(this); - } + RelationshipFilter build(); } } diff --git a/api/src/main/java/com/oviva/spicegen/api/SubjectRef.java b/api/src/main/java/com/oviva/spicegen/api/SubjectRef.java index aeabb45..e8b9c87 100644 --- a/api/src/main/java/com/oviva/spicegen/api/SubjectRef.java +++ b/api/src/main/java/com/oviva/spicegen/api/SubjectRef.java @@ -1,21 +1,16 @@ package com.oviva.spicegen.api; +import com.oviva.spicegen.api.internal.SubjectRefImpl; + public interface SubjectRef { String kind(); String id(); static SubjectRef ofObject(ObjectRef o) { - return new SubjectRef() { - @Override - public String kind() { - return o.kind(); - } - - @Override - public String id() { - return o.id(); - } - }; + if (o == null) { + return null; + } + return new SubjectRefImpl(o.kind(), o.id()); } } diff --git a/api/src/main/java/com/oviva/spicegen/api/UpdateRelationship.java b/api/src/main/java/com/oviva/spicegen/api/UpdateRelationship.java index 184592e..cc71cfe 100644 --- a/api/src/main/java/com/oviva/spicegen/api/UpdateRelationship.java +++ b/api/src/main/java/com/oviva/spicegen/api/UpdateRelationship.java @@ -1,101 +1,30 @@ package com.oviva.spicegen.api; -import java.util.Objects; +import com.oviva.spicegen.api.internal.UpdateRelationshipImpl; -public final class UpdateRelationship { - private final ObjectRef resource; +public interface UpdateRelationship { - private final String relation; - - private final SubjectRef subject; - - private final Operation operation; - - private UpdateRelationship( - ObjectRef resource, String relation, SubjectRef subject, Operation operation) { - this.resource = resource; - this.relation = relation; - this.subject = subject; - this.operation = operation; - } - - public static UpdateRelationship ofUpdate( - ObjectRef resource, String relation, ObjectRef subject) { - return new UpdateRelationship( - resource, relation, new ObjectRefSubject(subject), Operation.UPDATE); - } - - public static UpdateRelationship ofDelete( - ObjectRef resource, String relation, ObjectRef subject) { - return new UpdateRelationship( - resource, relation, new ObjectRefSubject(subject), Operation.DELETE); - } - - public SubjectRef subject() { - return subject; - } - - public ObjectRef resource() { - return resource; + static UpdateRelationship ofUpdate(ObjectRef resource, String relation, ObjectRef subject) { + return new UpdateRelationshipImpl( + resource, relation, SubjectRef.ofObject(subject), Operation.UPDATE); } - public String relation() { - return relation; + static UpdateRelationship ofDelete(ObjectRef resource, String relation, ObjectRef subject) { + return new UpdateRelationshipImpl( + resource, relation, SubjectRef.ofObject(subject), Operation.DELETE); } - public Operation operation() { - return operation; - } + SubjectRef subject(); - @Override - public String toString() { - var res = resource != null ? resource.toString() : ""; - var rel = relation != null ? relation : ""; - var sub = subject != null ? subject.toString() : ""; - return operation.name() + "(" + res + "#" + rel + "@" + sub + ")"; - } + ObjectRef resource(); - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - var that = (UpdateRelationship) o; - return Objects.equals(resource, that.resource) - && Objects.equals(relation, that.relation) - && Objects.equals(subject, that.subject) - && operation == that.operation; - } + String relation(); - @Override - public int hashCode() { - return Objects.hash(resource, relation, subject, operation); - } + Operation operation(); - public enum Operation { + enum Operation { UPDATE, DELETE } - - private static class ObjectRefSubject implements SubjectRef { - private final ObjectRef subject; - - private ObjectRefSubject(ObjectRef subject) { - this.subject = subject; - } - - @Override - public String kind() { - return subject.kind(); - } - - @Override - public String id() { - return subject.id(); - } - } } diff --git a/api/src/main/java/com/oviva/spicegen/api/UpdateRelationships.java b/api/src/main/java/com/oviva/spicegen/api/UpdateRelationships.java index c189976..3012535 100644 --- a/api/src/main/java/com/oviva/spicegen/api/UpdateRelationships.java +++ b/api/src/main/java/com/oviva/spicegen/api/UpdateRelationships.java @@ -1,62 +1,27 @@ package com.oviva.spicegen.api; -import java.util.ArrayList; +import com.oviva.spicegen.api.internal.UpdateRelationshipsImpl; import java.util.List; -public final class UpdateRelationships { +public interface UpdateRelationships { - private final List preconditions; - private final List updates; - - public static UpdateRelationshipsBuilder newBuilder() { - return new UpdateRelationshipsBuilder(); - } - - private UpdateRelationships(List updates, List preconditions) { - this.updates = List.copyOf(updates); - this.preconditions = List.copyOf(preconditions); - } - - public List updates() { - return updates; - } - - public List preconditions() { - return preconditions; + static Builder newBuilder() { + return new UpdateRelationshipsImpl.Builder(); } - public static final class UpdateRelationshipsBuilder { - private List preconditions = new ArrayList<>(); - private List updates = new ArrayList<>(); - - private UpdateRelationshipsBuilder() {} + List updates(); - public static UpdateRelationshipsBuilder newBuilder() { - return new UpdateRelationshipsBuilder(); - } + List preconditions(); - public UpdateRelationshipsBuilder preconditions(List preconditions) { - this.preconditions = preconditions; - return this; - } + interface Builder { + Builder preconditions(List preconditions); - public UpdateRelationshipsBuilder precondition(Precondition precondition) { - this.preconditions.add(precondition); - return this; - } + Builder precondition(Precondition precondition); - public UpdateRelationshipsBuilder updates(List updates) { - this.updates = updates; - return this; - } + Builder updates(List updates); - public UpdateRelationshipsBuilder update(UpdateRelationship updates) { - this.updates.add(updates); - return this; - } + Builder update(UpdateRelationship updates); - public UpdateRelationships build() { - return new UpdateRelationships(updates, preconditions); - } + UpdateRelationships build(); } } diff --git a/api/src/main/java/com/oviva/spicegen/api/internal/CheckPermissionImpl.java b/api/src/main/java/com/oviva/spicegen/api/internal/CheckPermissionImpl.java new file mode 100644 index 0000000..e5d92d4 --- /dev/null +++ b/api/src/main/java/com/oviva/spicegen/api/internal/CheckPermissionImpl.java @@ -0,0 +1,71 @@ +package com.oviva.spicegen.api.internal; + +import com.oviva.spicegen.api.CheckPermission; +import com.oviva.spicegen.api.Consistency; +import com.oviva.spicegen.api.ObjectRef; +import com.oviva.spicegen.api.SubjectRef; + +public record CheckPermissionImpl( + ObjectRef resource, String permission, SubjectRef subject, Consistency consistency) + implements CheckPermission { + + private CheckPermissionImpl(Builder builder) { + this(builder.resource, builder.permission, builder.subject, builder.consistency); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public ObjectRef resource() { + return resource; + } + + public String permission() { + return permission; + } + + public SubjectRef subject() { + return subject; + } + + public Consistency consistency() { + return consistency; + } + + public static final class Builder implements CheckPermission.Builder { + private ObjectRef resource; + private String permission; + private SubjectRef subject; + private Consistency consistency = Consistency.fullyConsistent(); + + @Override + public Builder resource(ObjectRef resource) { + this.resource = resource; + return this; + } + + @Override + public Builder permission(String permission) { + this.permission = permission; + return this; + } + + @Override + public Builder subject(SubjectRef subject) { + this.subject = subject; + return this; + } + + @Override + public Builder consistency(Consistency consistency) { + this.consistency = consistency; + return this; + } + + @Override + public CheckPermission build() { + return new CheckPermissionImpl(this); + } + } +} diff --git a/api/src/main/java/com/oviva/spicegen/api/internal/ConsistencyImpl.java b/api/src/main/java/com/oviva/spicegen/api/internal/ConsistencyImpl.java new file mode 100644 index 0000000..3330dd8 --- /dev/null +++ b/api/src/main/java/com/oviva/spicegen/api/internal/ConsistencyImpl.java @@ -0,0 +1,14 @@ +package com.oviva.spicegen.api.internal; + +import com.oviva.spicegen.api.Consistency; + +public record ConsistencyImpl(Consistency.Requirement requirement, String consistencyToken) + implements Consistency { + + private static final ConsistencyImpl FULLY_CONSISTENT = + new ConsistencyImpl(Consistency.Requirement.FULLY_CONSISTENT, null); + + public static ConsistencyImpl fullyConsistent() { + return FULLY_CONSISTENT; + } +} diff --git a/api/src/main/java/com/oviva/spicegen/api/internal/ObjectRefImpl.java b/api/src/main/java/com/oviva/spicegen/api/internal/ObjectRefImpl.java new file mode 100644 index 0000000..3305703 --- /dev/null +++ b/api/src/main/java/com/oviva/spicegen/api/internal/ObjectRefImpl.java @@ -0,0 +1,11 @@ +package com.oviva.spicegen.api.internal; + +import com.oviva.spicegen.api.ObjectRef; + +public record ObjectRefImpl(String kind, String id) implements ObjectRef { + + @Override + public String toString() { + return "%s:%s".formatted(kind, id); + } +} diff --git a/api/src/main/java/com/oviva/spicegen/api/internal/PreconditionImpl.java b/api/src/main/java/com/oviva/spicegen/api/internal/PreconditionImpl.java new file mode 100644 index 0000000..fa045db --- /dev/null +++ b/api/src/main/java/com/oviva/spicegen/api/internal/PreconditionImpl.java @@ -0,0 +1,37 @@ +package com.oviva.spicegen.api.internal; + +import com.oviva.spicegen.api.Precondition; +import com.oviva.spicegen.api.RelationshipFilter; + +public record PreconditionImpl(Precondition.Condition condition, RelationshipFilter filter) + implements Precondition { + + private PreconditionImpl(Builder builder) { + this(builder.condition, builder.filter); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder implements Precondition.Builder { + private Precondition.Condition condition; + private RelationshipFilter filter; + + @Override + public Builder condition(Precondition.Condition val) { + condition = val; + return this; + } + + @Override + public Builder filter(RelationshipFilter val) { + filter = val; + return this; + } + + public PreconditionImpl build() { + return new PreconditionImpl(this); + } + } +} diff --git a/api/src/main/java/com/oviva/spicegen/api/internal/RelationshipFilterImpl.java b/api/src/main/java/com/oviva/spicegen/api/internal/RelationshipFilterImpl.java new file mode 100644 index 0000000..9bc5323 --- /dev/null +++ b/api/src/main/java/com/oviva/spicegen/api/internal/RelationshipFilterImpl.java @@ -0,0 +1,100 @@ +package com.oviva.spicegen.api.internal; + +import com.oviva.spicegen.api.RelationshipFilter; +import java.util.Optional; + +public record RelationshipFilterImpl( + String resourceKind, + Optional resourceId, + Optional relation, + Optional subjectFilter) + implements RelationshipFilter { + + private RelationshipFilterImpl(Builder builder) { + this( + builder.resourceKind, + Optional.ofNullable(builder.resourceId), + Optional.ofNullable(builder.relation), + Optional.ofNullable(builder.subjectFilter)); + } + + public String resourceKind() { + return resourceKind; + } + + public record SubjectFilterImpl( + String subjectKind, Optional subjectId, Optional relation) + implements SubjectFilter { + + private SubjectFilterImpl(Builder builder) { + this( + builder.subjectKind, + Optional.ofNullable(builder.subjectId), + Optional.ofNullable(builder.relation)); + } + + public String subjectKind() { + return subjectKind; + } + + public static final class Builder implements SubjectFilter.Builder { + private String subjectKind; + private String subjectId; + private String relation; + + public Builder subjectKind(String val) { + subjectKind = val; + return this; + } + + public Builder subjectId(String val) { + subjectId = val; + return this; + } + + public Builder relation(String val) { + relation = val; + return this; + } + + public SubjectFilterImpl build() { + return new SubjectFilterImpl(this); + } + } + } + + public static final class Builder implements RelationshipFilter.Builder { + private String resourceKind; + private String resourceId; + private String relation; + private SubjectFilter subjectFilter; + + @Override + public Builder resourceKind(String val) { + resourceKind = val; + return this; + } + + @Override + public Builder resourceId(String val) { + resourceId = val; + return this; + } + + @Override + public Builder relation(String val) { + relation = val; + return this; + } + + @Override + public Builder subjectFilter(SubjectFilter val) { + this.subjectFilter = val; + return this; + } + + public RelationshipFilterImpl build() { + return new RelationshipFilterImpl(this); + } + } +} diff --git a/api/src/main/java/com/oviva/spicegen/api/internal/SubjectRefImpl.java b/api/src/main/java/com/oviva/spicegen/api/internal/SubjectRefImpl.java new file mode 100644 index 0000000..6f17f8b --- /dev/null +++ b/api/src/main/java/com/oviva/spicegen/api/internal/SubjectRefImpl.java @@ -0,0 +1,10 @@ +package com.oviva.spicegen.api.internal; + +import com.oviva.spicegen.api.SubjectRef; + +public record SubjectRefImpl(String kind, String id) implements SubjectRef { + @Override + public String toString() { + return "%s:%s".formatted(kind, id); + } +} diff --git a/api/src/main/java/com/oviva/spicegen/api/internal/UpdateRelationshipImpl.java b/api/src/main/java/com/oviva/spicegen/api/internal/UpdateRelationshipImpl.java new file mode 100644 index 0000000..1daf5bb --- /dev/null +++ b/api/src/main/java/com/oviva/spicegen/api/internal/UpdateRelationshipImpl.java @@ -0,0 +1,17 @@ +package com.oviva.spicegen.api.internal; + +import com.oviva.spicegen.api.ObjectRef; +import com.oviva.spicegen.api.SubjectRef; +import com.oviva.spicegen.api.UpdateRelationship; + +public record UpdateRelationshipImpl( + ObjectRef resource, String relation, SubjectRef subject, Operation operation) + implements UpdateRelationship { + @Override + public String toString() { + var res = resource != null ? resource.toString() : ""; + var rel = relation != null ? relation : ""; + var sub = subject != null ? subject.toString() : ""; + return operation.name() + "(" + res + "#" + rel + "@" + sub + ")"; + } +} diff --git a/api/src/main/java/com/oviva/spicegen/api/internal/UpdateRelationshipsImpl.java b/api/src/main/java/com/oviva/spicegen/api/internal/UpdateRelationshipsImpl.java new file mode 100644 index 0000000..0856a99 --- /dev/null +++ b/api/src/main/java/com/oviva/spicegen/api/internal/UpdateRelationshipsImpl.java @@ -0,0 +1,58 @@ +package com.oviva.spicegen.api.internal; + +import com.oviva.spicegen.api.Precondition; +import com.oviva.spicegen.api.UpdateRelationship; +import com.oviva.spicegen.api.UpdateRelationships; +import java.util.ArrayList; +import java.util.List; + +public record UpdateRelationshipsImpl( + List preconditions, List updates) + implements UpdateRelationships { + + public static Builder newBuilder() { + return new Builder(); + } + + public List updates() { + return updates; + } + + public List preconditions() { + return preconditions; + } + + public static final class Builder implements UpdateRelationships.Builder { + private List preconditions = new ArrayList<>(); + private List updates = new ArrayList<>(); + + @Override + public UpdateRelationships.Builder preconditions(List preconditions) { + this.preconditions = preconditions; + return this; + } + + @Override + public UpdateRelationships.Builder precondition(Precondition precondition) { + this.preconditions.add(precondition); + return this; + } + + @Override + public UpdateRelationships.Builder updates(List updates) { + this.updates = updates; + return this; + } + + @Override + public UpdateRelationships.Builder update(UpdateRelationship updates) { + this.updates.add(updates); + return this; + } + + @Override + public UpdateRelationships build() { + return new UpdateRelationshipsImpl(preconditions, updates); + } + } +} diff --git a/api/src/test/java/com/oviva/spicegen/api/CheckPermissionTest.java b/api/src/test/java/com/oviva/spicegen/api/CheckPermissionTest.java new file mode 100644 index 0000000..3729588 --- /dev/null +++ b/api/src/test/java/com/oviva/spicegen/api/CheckPermissionTest.java @@ -0,0 +1,74 @@ +package com.oviva.spicegen.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.Test; + +public class CheckPermissionTest { + + @Test + public void test_build_success() { + + var namespace = "tenant"; + var id = "9392"; + var consistencyToken = "t0ken"; + var permission = "write"; + + var objectRef = ObjectRef.of(namespace, id); + var subjectRef = SubjectRef.ofObject(ObjectRef.of(namespace, id)); + + var checkPermission = + CheckPermission.newBuilder() + .consistency(Consistency.atLeastAsFreshAs(consistencyToken)) + .permission(permission) + .resource(objectRef) + .subject(subjectRef) + .build(); + + assertEquals(checkPermission.consistency().consistencyToken(), consistencyToken); + assertEquals(checkPermission.permission(), permission); + assertEquals(checkPermission.resource(), objectRef); + assertEquals(checkPermission.subject(), subjectRef); + } + + @Test + void of_hashCode() { + var h1 = createCheck("17").hashCode(); + var h2 = createCheck("17").hashCode(); + + assertEquals(h1, h2); + } + + private CheckPermission createCheck(String subjectId) { + + return CheckPermission.newBuilder() + .permission("test") + .resource(ObjectRef.of("tenant", "1")) + .subject(SubjectRef.ofObject(ObjectRef.of("user", subjectId))) + .build(); + } + + @Test + void of_equals_same() { + var c1 = createCheck("1"); + + assertEquals(c1, c1); + } + + @Test + void of_equals() { + var c1 = createCheck("1"); + var c2 = createCheck("1"); + + assertEquals(c1, c2); + } + + @Test + void of_equals_notEqual() { + var c1 = createCheck("3"); + var c2 = createCheck("4"); + + assertNotEquals(c1, c2); + } +} diff --git a/api/src/test/java/com/oviva/spicegen/api/SubjectRefTest.java b/api/src/test/java/com/oviva/spicegen/api/SubjectRefTest.java new file mode 100644 index 0000000..bdc37ba --- /dev/null +++ b/api/src/test/java/com/oviva/spicegen/api/SubjectRefTest.java @@ -0,0 +1,65 @@ +package com.oviva.spicegen.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class SubjectRefTest { + + @Test + void ofUser() { + var uuid = UUID.fromString("20162b05-fbc5-4567-853f-7ad90fc29d25"); + var user = SubjectRef.ofObject(ObjectRef.of("user", uuid.toString())); + + assertEquals(user.id(), uuid.toString()); + assertEquals(user.kind(), "user"); + } + + @Test + void ofUuid() { + var namespace = "tenant"; + var uuid = UUID.fromString("c0fe2b05-fbc5-4567-853f-7ad90fc29d25"); + var user = SubjectRef.ofObject(ObjectRef.of(namespace, uuid.toString())); + + assertEquals(user.id(), uuid.toString()); + assertEquals(user.kind(), namespace); + } + + @Test + void ofUuid_nullId() { + assertThrows( + IllegalArgumentException.class, () -> SubjectRef.ofObject(ObjectRef.of("anotherns", null))); + } + + @Test + void of() { + var namespace = "tenant"; + var id = "9392"; + var user = SubjectRef.ofObject(ObjectRef.of(namespace, id)); + + assertEquals(user.id(), id); + assertEquals(user.kind(), namespace); + } + + @Test + void of_nullId() { + assertThrows( + IllegalArgumentException.class, () -> SubjectRef.ofObject(ObjectRef.of("somens", null))); + } + + @Test + void of_nullNamespace() { + assertThrows( + IllegalArgumentException.class, () -> SubjectRef.ofObject(ObjectRef.of(null, "32"))); + } + + @Test + void equals() { + var a = SubjectRef.ofObject(ObjectRef.of("a", "1")); + var b = SubjectRef.ofObject(ObjectRef.of("a", "1")); + + assertEquals(a, b); + } +} diff --git a/api/src/test/java/com/oviva/spicegen/api/UpdateRelationshipTest.java b/api/src/test/java/com/oviva/spicegen/api/UpdateRelationshipTest.java new file mode 100644 index 0000000..8c9d5b5 --- /dev/null +++ b/api/src/test/java/com/oviva/spicegen/api/UpdateRelationshipTest.java @@ -0,0 +1,147 @@ +package com.oviva.spicegen.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class UpdateRelationshipTest { + + private static final String TENANT = "tenant"; + private static final String TENANT_ID = "8288"; + + private static final String DOCUMENT = "document"; + private static final String DOCUMENT_ID = "138"; + private static final String ADMINISTRATOR = "administrator"; + private static final String NAMESPACE_ID = "9392"; + + @Test + void test_ofUpdate() { + + var resource = ObjectRef.of(TENANT, NAMESPACE_ID); + var subject = ObjectRef.of(TENANT, NAMESPACE_ID); + + var updateRelationship = UpdateRelationship.ofUpdate(resource, ADMINISTRATOR, subject); + + assertEquals(updateRelationship.operation(), UpdateRelationship.Operation.UPDATE); + assertEquals(updateRelationship.relation(), ADMINISTRATOR); + assertEquals(updateRelationship.resource(), resource); + assertEquals(updateRelationship.subject(), SubjectRef.ofObject(subject)); + } + + @Test + void test_ofDelete() { + + var resource = ObjectRef.of(TENANT, NAMESPACE_ID); + var subject = ObjectRef.of(TENANT, NAMESPACE_ID); + + var updateRelationship = UpdateRelationship.ofDelete(resource, ADMINISTRATOR, subject); + + assertEquals(updateRelationship.operation(), UpdateRelationship.Operation.DELETE); + assertEquals(updateRelationship.relation(), ADMINISTRATOR); + assertEquals(updateRelationship.resource(), resource); + assertEquals(updateRelationship.subject(), SubjectRef.ofObject(subject)); + } + + @Test + void of_hashCode() { + var h1 = + UpdateRelationship.ofUpdate( + ObjectRef.of(DOCUMENT, DOCUMENT_ID), ADMINISTRATOR, ObjectRef.of(TENANT, TENANT_ID)); + + var h2 = + UpdateRelationship.ofUpdate( + ObjectRef.of(DOCUMENT, DOCUMENT_ID), ADMINISTRATOR, ObjectRef.of(TENANT, TENANT_ID)); + + assertEquals(h1.hashCode(), h2.hashCode()); + } + + @Test + void of_equals_same() { + var u1 = + UpdateRelationship.ofUpdate( + ObjectRef.of(DOCUMENT, DOCUMENT_ID), ADMINISTRATOR, ObjectRef.of(TENANT, TENANT_ID)); + + assertEquals(u1, u1); + } + + @Test + void of_equals() { + var uuid = UUID.randomUUID(); + var u1 = createUpdateDocumentOwnedByUser(uuid); + var u2 = createUpdateDocumentOwnedByUser(uuid); + + assertEquals(u1, u2); + } + + @Test + void of_equals_notEqual() { + var u1 = createUpdateDocumentOwnedByUser(UUID.randomUUID()); + var u2 = createUpdateDocumentOwnedByUser(UUID.randomUUID()); + + assertNotEquals(u1, u2); + } + + @Test + void of_equals_notEqual_subjectKind() { + var subjectUuid = UUID.randomUUID(); + + var u1 = createUpdateDocumentOwnedByUser(subjectUuid); + var u2 = + UpdateRelationship.ofUpdate( + u1.resource(), u1.relation(), ObjectRef.of("another", u1.subject().id())); + + assertNotEquals(u1, u2); + } + + @Test + void of_equals_notEqual_subjectId() { + var subjectUuid = UUID.randomUUID(); + + var u1 = createUpdateDocumentOwnedByUser(subjectUuid); + var u2 = + UpdateRelationship.ofUpdate( + u1.resource(), u1.relation(), ObjectRef.of(u1.subject().kind(), "992982")); + + assertNotEquals(u1, u2); + } + + @Test + void of_equals_notEqual_null() { + var u1 = createUpdateDocumentOwnedByUser(UUID.randomUUID()); + var u2 = (UpdateRelationship) null; + + assertNotEquals(u1, u2); + } + + @Test + void of_equals_notEqualOtherClass() { + var u1 = createUpdateDocumentOwnedByUser(UUID.randomUUID()); + var u2 = new Object(); + + assertNotEquals(u1, u2); + } + + private UpdateRelationship createUpdateDocumentOwnedByUser(UUID subjectUuid) { + + return UpdateRelationship.ofUpdate( + ObjectRef.of("document", "1"), "owner", ObjectRef.of("user", subjectUuid.toString())); + } + + @Test + void of_toString() { + var obj = ObjectRef.of("document", "4"); + var sub = ObjectRef.of("user", "18"); + var relation = "owner"; + + var u = UpdateRelationship.ofUpdate(obj, relation, sub); + assertEquals("UPDATE(document:4#owner@user:18)", u.toString()); + } + + @Test + void of_toString_null() { + var u = UpdateRelationship.ofUpdate(null, null, null); + assertEquals("UPDATE(#@)", u.toString()); + } +} diff --git a/api/src/test/java/com/oviva/spicegen/api/UpdateRelationshipsTest.java b/api/src/test/java/com/oviva/spicegen/api/UpdateRelationshipsTest.java new file mode 100644 index 0000000..7346046 --- /dev/null +++ b/api/src/test/java/com/oviva/spicegen/api/UpdateRelationshipsTest.java @@ -0,0 +1,111 @@ +package com.oviva.spicegen.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +public class UpdateRelationshipsTest { + + private static final String PATIENT_ID = UUID.randomUUID().toString().replaceAll("-", ""); + private static final String TENANT_ID = UUID.randomUUID().toString().replaceAll("-", ""); + private static final String PATIENT = "patient"; + private static final String TENANT = "tenant"; + private static final String USER = "user"; + private static final String ADMINISTRATOR = "administrator"; + + @Test + public void test_updateRelationships_ownBuilder() { + + var updates = getUpdateRelationshipList(); + var preconditions = getPreconditionList(); + + var updateRelationships = + UpdateRelationships.newBuilder().updates(updates).preconditions(preconditions).build(); + + assertEquals(updateRelationships.updates(), updates); + assertEquals(updateRelationships.preconditions(), preconditions); + } + + @Test + public void test_updateRelationshipsBuilder() { + var updates = getUpdateRelationshipList(); + var preconditions = getPreconditionList(); + + var updateRelationships = + UpdateRelationships.newBuilder().updates(updates).preconditions(preconditions).build(); + + assertEquals(updateRelationships.updates(), updates); + assertEquals(updateRelationships.preconditions(), preconditions); + } + + @Test + public void test_updateRelationshipsBuilder_withAddOperations() { + + var userId = UUID.randomUUID(); + var subject = ObjectRef.of("user", userId.toString()); + var resource = ObjectRef.of(TENANT, TENANT_ID); + + var precondition = + Precondition.mustNotMatch( + RelationshipFilter.newBuilder() + .resourceKind(TENANT) + .relation(PATIENT) + .subjectFilter( + RelationshipFilter.SubjectFilter.newBuilder() + .subjectKind(USER) + .subjectId(userId.toString()) + .build()) + .build()); + + var updateRelationship = UpdateRelationship.ofUpdate(resource, ADMINISTRATOR, subject); + + var updateRelationships = + UpdateRelationships.newBuilder() + .precondition(precondition) + .update(updateRelationship) + .build(); + + assertEquals(updateRelationships.updates().size(), 1); + assertEquals(updateRelationships.updates().get(0), updateRelationship); + assertEquals(updateRelationships.preconditions().size(), 1); + assertEquals(updateRelationships.preconditions().get(0), precondition); + } + + private List getPreconditionList() { + + var subjectFilter = + RelationshipFilter.SubjectFilter.newBuilder() + .subjectKind(TENANT) + .subjectId(TENANT_ID) + .build(); + + var relationshipFilter = + RelationshipFilter.newBuilder() + .resourceKind(PATIENT) + .resourceId(PATIENT_ID) + .relation(TENANT) + .subjectFilter(subjectFilter) + .build(); + + var precondition = + Precondition.newBuilder() + .condition(Precondition.Condition.MUST_MATCH) + .filter(relationshipFilter) + .build(); + + return List.of(precondition); + } + + private List getUpdateRelationshipList() { + var id = "9392"; + + var resource = ObjectRef.of(TENANT, id); + var subject = ObjectRef.of(TENANT, id); + + var updateRelationship = UpdateRelationship.ofUpdate(resource, ADMINISTRATOR, subject); + + return List.of(updateRelationship); + } +} diff --git a/example/src/test/java/com/oviva/spicegen/example/ExampleTest.java b/example/src/test/java/com/oviva/spicegen/example/ExampleTest.java index 8fc84ea..c4b6a7a 100644 --- a/example/src/test/java/com/oviva/spicegen/example/ExampleTest.java +++ b/example/src/test/java/com/oviva/spicegen/example/ExampleTest.java @@ -1,19 +1,12 @@ package com.oviva.spicegen.example; -import static com.authzed.api.v1.PermissionService.CheckPermissionResponse.Permissionship.PERMISSIONSHIP_HAS_PERMISSION; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; -import com.authzed.api.v1.Core; import com.authzed.api.v1.PermissionsServiceGrpc; import com.authzed.api.v1.SchemaServiceGrpc; import com.authzed.api.v1.SchemaServiceOuterClass; import com.authzed.grpcutil.BearerToken; -import com.oviva.spicegen.api.ObjectRef; -import com.oviva.spicegen.api.PermissionService; -import com.oviva.spicegen.api.SubjectRef; -import com.oviva.spicegen.api.UpdateRelationships; -import com.oviva.spicegen.permissions.SchemaConstants; +import com.oviva.spicegen.api.*; import com.oviva.spicegen.permissions.refs.DocumentRef; import com.oviva.spicegen.permissions.refs.FolderRef; import com.oviva.spicegen.permissions.refs.UserRef; @@ -107,42 +100,10 @@ void example() { var consistencyToken = updateResult.consistencyToken(); // EXAMPLE: checking permission - var res = - checkPermission( - document, - // note the generated constants! - SchemaConstants.PERMISSION_DOCUMENT_READ, - SubjectRef.ofObject(user), - consistencyToken); - - assertEquals(PERMISSIONSHIP_HAS_PERMISSION, res.getPermissionship()); - } - - private com.authzed.api.v1.PermissionService.CheckPermissionResponse checkPermission( - ObjectRef object, String permission, SubjectRef subject, String consistencyToken) { - - return permissionServiceStub.checkPermission( - com.authzed.api.v1.PermissionService.CheckPermissionRequest.newBuilder() - .setPermission(permission) - .setResource( - Core.ObjectReference.newBuilder() - .setObjectType(object.kind()) - .setObjectId(object.id()) - .build()) - .setSubject( - Core.SubjectReference.newBuilder() - .setObject( - Core.ObjectReference.newBuilder() - .setObjectType(subject.kind()) - .setObjectId(subject.id()) - .build()) - .build()) - .setConsistency( - com.authzed.api.v1.PermissionService.Consistency.newBuilder() - .setAtLeastAsFresh( - Core.ZedToken.newBuilder().setToken(consistencyToken).build()) - .build()) - .build()); + assertTrue( + permissionService.checkPermission( + document.checkRead( + SubjectRef.ofObject(user), Consistency.atLeastAsFreshAs(consistencyToken)))); } private String loadSchema() { diff --git a/generator/src/main/java/com/oviva/spicegen/generator/internal/SpiceDbClientGeneratorImpl.java b/generator/src/main/java/com/oviva/spicegen/generator/internal/SpiceDbClientGeneratorImpl.java index 9d2816c..a7f141e 100644 --- a/generator/src/main/java/com/oviva/spicegen/generator/internal/SpiceDbClientGeneratorImpl.java +++ b/generator/src/main/java/com/oviva/spicegen/generator/internal/SpiceDbClientGeneratorImpl.java @@ -2,8 +2,7 @@ import static com.oviva.spicegen.generator.utils.TextUtils.toPascalCase; -import com.oviva.spicegen.api.ObjectRef; -import com.oviva.spicegen.api.UpdateRelationship; +import com.oviva.spicegen.api.*; import com.oviva.spicegen.generator.Options; import com.oviva.spicegen.generator.SpiceDbClientGenerator; import com.oviva.spicegen.generator.utils.TextUtils; @@ -160,11 +159,43 @@ private void generateRefs(Schema spec) { addUpdateMethods(typedRefBuilder, definition); + addCheckMethods(typedRefBuilder, definition); + typedRef = typedRefBuilder.build(); writeSource(typedRef, ".refs"); } } + private void addCheckMethods(TypeSpec.Builder typeRefBuilder, ObjectDefinition definition) { + for (Permission permission : definition.permissions()) { + + var permissionName = TextUtils.toPascalCase(permission.name()); + var checkMethod = "check" + permissionName; + + var subjectParamName = "subject"; + var consistencyParamName = "consistency"; + + typeRefBuilder.addMethod( + MethodSpec.methodBuilder(checkMethod) + .addModifiers(Modifier.PUBLIC) + .addParameter(ClassName.get(SubjectRef.class), subjectParamName) + .addParameter(ClassName.get(Consistency.class), consistencyParamName) + .returns(ClassName.get(CheckPermission.class)) + .addCode( + """ + if ($L == null) { + throw new IllegalArgumentException("subject must not be null"); + } + return CheckPermission.newBuilder().resource(this).permission($S).subject($L).consistency($L).build(); + """, + subjectParamName, + permission.name(), + subjectParamName, + consistencyParamName) + .build()); + } + } + private void addUpdateMethods(TypeSpec.Builder typeRefBuilder, ObjectDefinition definition) { for (Relation relation : definition.relations()) { diff --git a/model/pom.xml b/model/pom.xml index 69204bf..43e7a1a 100644 --- a/model/pom.xml +++ b/model/pom.xml @@ -40,11 +40,6 @@ junit-jupiter test - - org.hamcrest - hamcrest - test - diff --git a/model/src/main/resources/spicegen_darwin_amd64_v1 b/model/src/main/resources/spicegen_darwin_amd64_v1 index 07659b5..7640d83 100755 Binary files a/model/src/main/resources/spicegen_darwin_amd64_v1 and b/model/src/main/resources/spicegen_darwin_amd64_v1 differ diff --git a/model/src/main/resources/spicegen_darwin_arm64 b/model/src/main/resources/spicegen_darwin_arm64 index 7e3d3d0..1838cfc 100755 Binary files a/model/src/main/resources/spicegen_darwin_arm64 and b/model/src/main/resources/spicegen_darwin_arm64 differ diff --git a/model/src/main/resources/spicegen_linux_386 b/model/src/main/resources/spicegen_linux_386 index d1409c3..bcbbac1 100755 Binary files a/model/src/main/resources/spicegen_linux_386 and b/model/src/main/resources/spicegen_linux_386 differ diff --git a/model/src/main/resources/spicegen_linux_amd64_v1 b/model/src/main/resources/spicegen_linux_amd64_v1 index 858b484..97036f9 100755 Binary files a/model/src/main/resources/spicegen_linux_amd64_v1 and b/model/src/main/resources/spicegen_linux_amd64_v1 differ diff --git a/model/src/main/resources/spicegen_linux_arm64 b/model/src/main/resources/spicegen_linux_arm64 index aa41a01..5a06506 100755 Binary files a/model/src/main/resources/spicegen_linux_arm64 and b/model/src/main/resources/spicegen_linux_arm64 differ diff --git a/model/src/test/java/com/oviva/spicegen/parser/SpiceDbSchemaParserTest.java b/model/src/test/java/com/oviva/spicegen/parser/SpiceDbSchemaParserTest.java index ea001de..ee90be4 100644 --- a/model/src/test/java/com/oviva/spicegen/parser/SpiceDbSchemaParserTest.java +++ b/model/src/test/java/com/oviva/spicegen/parser/SpiceDbSchemaParserTest.java @@ -1,7 +1,5 @@ package com.oviva.spicegen.parser; -import static org.hamcrest.Matchers.*; - import java.nio.file.Path; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; diff --git a/parser/main.go b/parser/main.go index 4366e5f..65e1229 100644 --- a/parser/main.go +++ b/parser/main.go @@ -36,7 +36,7 @@ func main() { out := os.Stdout if outPath != "-" { - f, err := os.OpenFile(outPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + f, err := os.OpenFile(outPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { log.Fatal(err) } @@ -65,7 +65,7 @@ func main() { func Parse(sourceName string, r io.Reader) (Node, error) { b, err := io.ReadAll(r) if err != nil { - return nil, fmt.Errorf("failed to read schema %s", sourceName) + return nil, fmt.Errorf("failed to read schema %s: %w", sourceName, err) } rawRoot := parser.Parse(createAstNode, input.Source(sourceName), string(b)) diff --git a/pom.xml b/pom.xml index dbce9bb..0492e90 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,6 @@ 1.7.36 - 2.2 2.17.0 5.10.2 ${mockito.version} @@ -150,12 +149,6 @@ ${jupiter.version} test - - org.hamcrest - hamcrest - ${hamcrest.version} - test - org.mockito mockito-core diff --git a/spicedb-binding/pom.xml b/spicedb-binding/pom.xml index d9e808c..2d04dcf 100644 --- a/spicedb-binding/pom.xml +++ b/spicedb-binding/pom.xml @@ -43,11 +43,6 @@ junit-jupiter test - - org.hamcrest - hamcrest - test - org.mockito mockito-core @@ -69,6 +64,11 @@ testcontainers test + + org.testcontainers + postgresql + test + org.testcontainers junit-jupiter diff --git a/spicedb-binding/src/main/docker/compose.dev.yaml b/spicedb-binding/src/main/docker/compose.dev.yaml deleted file mode 100644 index 511ba25..0000000 --- a/spicedb-binding/src/main/docker/compose.dev.yaml +++ /dev/null @@ -1,43 +0,0 @@ -version: "3.9" -services: - spicedb: - image: quay.io/authzed/spicedb - ports: - - "127.0.0.1::8080" - - "127.0.0.1::50051" - - "127.0.0.1::9090" - command: - - 'serve' - - '--grpc-preshared-key=t0ken' - - '--datastore-engine=postgres' - - '--datastore-conn-uri=postgres://spicedb-pg:5432/spicedb?sslmode=disable&user=postgres&password=root' - depends_on: - spicedb-pg: - condition: service_healthy - spicedb-migrator: - condition: service_completed_successfully - spicedb-migrator: - image: quay.io/authzed/spicedb - command: - - 'migrate' - - 'head' - - '--datastore-engine' - - 'postgres' - - '--datastore-conn-uri' - - 'postgres://spicedb-pg:5432/spicedb?sslmode=disable&user=postgres&password=root' - depends_on: - spicedb-pg: - condition: service_healthy - spicedb-pg: - image: postgres:14 - ports: - - "127.0.0.1::5432" - environment: - POSTGRES_DB: 'spicedb' - POSTGRES_PASSWORD: 'root' - healthcheck: - test: "psql 'postgres://spicedb-pg:5432/spicedb?sslmode=disable&user=postgres&password=root' --quiet --output=/dev/null -c 'SELECT 1;'" - interval: 1s - timeout: 1s - retries: 3 - start_period: 10s \ No newline at end of file diff --git a/spicedb-binding/src/main/java/com/oviva/spicegen/spicedbbinding/internal/SpiceDbPermissionServiceImpl.java b/spicedb-binding/src/main/java/com/oviva/spicegen/spicedbbinding/internal/SpiceDbPermissionServiceImpl.java index fa52576..e7c3101 100644 --- a/spicedb-binding/src/main/java/com/oviva/spicegen/spicedbbinding/internal/SpiceDbPermissionServiceImpl.java +++ b/spicedb-binding/src/main/java/com/oviva/spicegen/spicedbbinding/internal/SpiceDbPermissionServiceImpl.java @@ -1,20 +1,21 @@ package com.oviva.spicegen.spicedbbinding.internal; +import com.authzed.api.v1.Core; import com.authzed.api.v1.PermissionsServiceGrpc; -import com.oviva.spicegen.api.PermissionService; -import com.oviva.spicegen.api.UpdateRelationships; -import com.oviva.spicegen.api.UpdateResult; +import com.oviva.spicegen.api.*; import io.grpc.StatusRuntimeException; public class SpiceDbPermissionServiceImpl implements PermissionService { - private final CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapper updateRelationshipMapper = - new CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapper(); + private final UpdateRelationshipMapper updateRelationshipMapper = new UpdateRelationshipMapper(); private final PreconditionMapper preconditionMapper = new PreconditionMapper(); private final PermissionsServiceGrpc.PermissionsServiceBlockingStub permissionsService; private final GrpcExceptionMapper exceptionMapper = new GrpcExceptionMapper(); + private final ObjectReferenceMapper objectReferenceMapper = new ObjectReferenceMapper(); + private final SubjectReferenceMapper subjectReferenceMapper = new SubjectReferenceMapper(); + public SpiceDbPermissionServiceImpl( PermissionsServiceGrpc.PermissionsServiceBlockingStub permissionsService) { this.permissionsService = permissionsService; @@ -41,4 +42,46 @@ public UpdateResult updateRelationships(UpdateRelationships updates) { throw exceptionMapper.map(e); } } + + @Override + public boolean checkPermission(CheckPermission checkPermission) { + + var request = mapCheckPermission(checkPermission); + + try { + var response = permissionsService.checkPermission(request); + return response.getPermissionship() + == com.authzed.api.v1.PermissionService.CheckPermissionResponse.Permissionship + .PERMISSIONSHIP_HAS_PERMISSION; + } catch (StatusRuntimeException e) { + throw exceptionMapper.map(e); + } + } + + private com.authzed.api.v1.PermissionService.CheckPermissionRequest mapCheckPermission( + CheckPermission checkPermission) { + + var consistency = mapConsistency(checkPermission.consistency()); + + return com.authzed.api.v1.PermissionService.CheckPermissionRequest.newBuilder() + .setConsistency(consistency) + .setResource(objectReferenceMapper.map(checkPermission.resource())) + .setSubject(subjectReferenceMapper.map(checkPermission.subject())) + .setPermission(checkPermission.permission()) + .build(); + } + + private com.authzed.api.v1.PermissionService.Consistency mapConsistency(Consistency consistency) { + return switch (consistency.requirement()) { + case FULLY_CONSISTENT -> + com.authzed.api.v1.PermissionService.Consistency.newBuilder() + .setFullyConsistent(true) + .build(); + case AT_LEAST_AS_FRESH -> + com.authzed.api.v1.PermissionService.Consistency.newBuilder() + .setAtLeastAsFresh( + Core.ZedToken.newBuilder().setToken(consistency.consistencyToken()).build()) + .build(); + }; + } } diff --git a/spicedb-binding/src/main/java/com/oviva/spicegen/spicedbbinding/internal/CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapper.java b/spicedb-binding/src/main/java/com/oviva/spicegen/spicedbbinding/internal/UpdateRelationshipMapper.java similarity index 94% rename from spicedb-binding/src/main/java/com/oviva/spicegen/spicedbbinding/internal/CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapper.java rename to spicedb-binding/src/main/java/com/oviva/spicegen/spicedbbinding/internal/UpdateRelationshipMapper.java index 3cf34ec..ad72f41 100644 --- a/spicedb-binding/src/main/java/com/oviva/spicegen/spicedbbinding/internal/CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapper.java +++ b/spicedb-binding/src/main/java/com/oviva/spicegen/spicedbbinding/internal/UpdateRelationshipMapper.java @@ -3,7 +3,7 @@ import com.authzed.api.v1.Core; import com.oviva.spicegen.api.UpdateRelationship; -public class CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapper { +public class UpdateRelationshipMapper { private final ObjectReferenceMapper objectReferenceMapper = new ObjectReferenceMapper(); private final SubjectReferenceMapper subjectReferenceMapper = new SubjectReferenceMapper(); diff --git a/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/SpiceDbPermissionServiceBuilderTest.java b/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/SpiceDbPermissionServiceBuilderTest.java index 8c8624d..7b73709 100644 --- a/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/SpiceDbPermissionServiceBuilderTest.java +++ b/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/SpiceDbPermissionServiceBuilderTest.java @@ -1,7 +1,6 @@ package com.oviva.spicegen.spicedbbinding; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import com.authzed.api.v1.PermissionsServiceGrpc; import com.authzed.grpcutil.BearerToken; @@ -28,6 +27,6 @@ public void test_spiceDbPermissionServiceBuilder() { .permissionsBlockingStub(permissionsService) .build(); - assertThat(svc, instanceOf(PermissionService.class)); + assertInstanceOf(PermissionService.class, svc); } } diff --git a/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapperTest.java b/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapperTest.java deleted file mode 100644 index 2c7787f..0000000 --- a/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapperTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.oviva.spicegen.spicedbbinding.internal; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.notNullValue; - -import com.authzed.api.v1.Core; -import com.oviva.spicegen.api.ObjectRef; -import com.oviva.spicegen.api.UpdateRelationship; -import org.junit.jupiter.api.Test; - -public class CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapperTest { - - private static final String TENANT = "tenant"; - private static final String USER = "user"; - private static final String ADMINISTRATOR = "administrator"; - private static final String ID = "9392"; - - private final CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapper mapper = - new CreateRelationshipUpdateToSpiceDBUpdateRelationshipMapper(); - - @Test - public void test_mapper_withUpdateOperation() { - - var resource = ObjectRef.of(TENANT, ID); - var subject = ObjectRef.of(USER, ID); - - var updateRelationship = UpdateRelationship.ofUpdate(resource, ADMINISTRATOR, subject); - var map = mapper.map(updateRelationship); - - assertThat(map.getOperation(), equalTo(Core.RelationshipUpdate.Operation.OPERATION_TOUCH)); - assertThat(map.getRelationship(), notNullValue()); - assertThat(map.getRelationship().getRelation(), equalTo(ADMINISTRATOR)); - assertThat(map.getRelationship().getResource().getObjectId(), equalTo(ID)); - assertThat(map.getRelationship().getResource().getObjectType(), equalTo(TENANT)); - } - - @Test - public void test_mapper_withDeleteOperation() { - - var resource = ObjectRef.of(TENANT, ID); - var subject = ObjectRef.of(USER, ID); - - var updateRelationship = UpdateRelationship.ofDelete(resource, ADMINISTRATOR, subject); - var map = mapper.map(updateRelationship); - - assertThat(map.getOperation(), equalTo(Core.RelationshipUpdate.Operation.OPERATION_DELETE)); - assertThat(map.getRelationship(), notNullValue()); - assertThat(map.getRelationship().getRelation(), equalTo(ADMINISTRATOR)); - assertThat(map.getRelationship().getResource().getObjectId(), equalTo(ID)); - assertThat(map.getRelationship().getResource().getObjectType(), equalTo(TENANT)); - } -} diff --git a/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/GrpcExceptionMapperTest.java b/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/GrpcExceptionMapperTest.java index 3c4496f..e0c8cc0 100644 --- a/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/GrpcExceptionMapperTest.java +++ b/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/GrpcExceptionMapperTest.java @@ -1,7 +1,7 @@ package com.oviva.spicegen.spicedbbinding.internal; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.oviva.spicegen.api.exceptions.*; import io.grpc.Status; @@ -16,24 +16,24 @@ public class GrpcExceptionMapperTest { public void test_map_permissionDenied() { var exception = grpcExceptionMapper.map(new StatusRuntimeException(Status.PERMISSION_DENIED)); - assertThat(exception, instanceOf(AuthorizationException.class)); - assertThat(exception.getMessage(), equalTo("permission denied")); + assertTrue(exception instanceof AuthorizationException); + assertEquals(exception.getMessage(), "permission denied"); } @Test public void test_map_unauthenticated() { var exception = grpcExceptionMapper.map(new StatusRuntimeException(Status.UNAUTHENTICATED)); - assertThat(exception, instanceOf(AuthenticationException.class)); - assertThat(exception.getMessage(), equalTo("unauthenticated")); + assertTrue(exception instanceof AuthenticationException); + assertEquals(exception.getMessage(), "unauthenticated"); } @Test public void test_map_alreadyExists() { var exception = grpcExceptionMapper.map(new StatusRuntimeException(Status.ALREADY_EXISTS)); - assertThat(exception, instanceOf(ConflictException.class)); - assertThat(exception.getMessage(), equalTo("already exists")); + assertTrue(exception instanceof ConflictException); + assertEquals(exception.getMessage(), "already exists"); } @Test @@ -41,23 +41,23 @@ public void test_map_invalidArgument() { var exception = grpcExceptionMapper.map(new StatusRuntimeException(Status.INVALID_ARGUMENT)); - assertThat(exception, instanceOf(ValidationException.class)); - assertThat(exception.getMessage(), equalTo("invalid argument")); + assertTrue(exception instanceof ValidationException); + assertEquals(exception.getMessage(), "invalid argument"); } @Test public void test_map_failedPrecondition() { var exception = grpcExceptionMapper.map(new StatusRuntimeException(Status.FAILED_PRECONDITION)); - assertThat(exception, instanceOf(ValidationException.class)); - assertThat(exception.getMessage(), equalTo("failed precondition")); + assertTrue(exception instanceof ValidationException); + assertEquals(exception.getMessage(), "failed precondition"); } @Test public void test_map_unexpectedValue() { var exception = grpcExceptionMapper.map(new StatusRuntimeException(Status.CANCELLED)); - assertThat(exception, instanceOf(ClientException.class)); - assertThat(exception.getMessage(), containsString("unexpected status:")); + assertTrue(exception instanceof ClientException); + assertTrue(exception.getMessage().startsWith("unexpected status:")); } } diff --git a/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/SpiceDbContractTestContextProvider.java b/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/SpiceDbContractTestContextProvider.java index 410dd83..6df6103 100644 --- a/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/SpiceDbContractTestContextProvider.java +++ b/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/SpiceDbContractTestContextProvider.java @@ -6,17 +6,23 @@ import com.oviva.spicegen.spicedbbinding.test.GenericTypedParameterResolver; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; -import java.io.File; +import java.time.Duration; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import org.junit.jupiter.api.extension.*; -import org.testcontainers.containers.DockerComposeContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.utility.DockerImageName; public class SpiceDbContractTestContextProvider implements TestTemplateInvocationContextProvider { + private static Logger logger = LoggerFactory.getLogger(SpiceDbContractTestContextProvider.class); private static final String TOKEN = "t0ken"; private static final int GRPC_PORT = 50051; @@ -35,14 +41,10 @@ public Stream provideTestTemplateInvocationContex private TestTemplateInvocationContext inMemorySpiceDB() { var spicedb = - new GenericContainer<>(DockerImageName.parse("quay.io/authzed/spicedb:v1.32.0")) - .withCommand("serve", "--grpc-preshared-key", TOKEN) - .withExposedPorts( - GRPC_PORT, // grpc - 8080, // dashboard - 9090 // metrics - ); - + createSpicedbBaseContainer("serve", "--grpc-preshared-key", TOKEN) + .withExposedPorts(GRPC_PORT) + .waitingFor( + new LogMessageWaitStrategy().withRegEx(".*\"grpc server started serving\".*")); spicedb.start(); var host = spicedb.getHost(); @@ -61,22 +63,45 @@ private TestTemplateInvocationContext inMemorySpiceDB() { private TestTemplateInvocationContext postgresSpiceDB() { - var spiceDbServiceName = "spicedb_1"; - var environment = - new DockerComposeContainer<>(new File("src/main/docker/compose.dev.yaml")) - .withExposedService(spiceDbServiceName, GRPC_PORT); - - environment.start(); + var net = Network.newNetwork(); + + var migrator = + createSpicedbBaseContainer( + "migrate", + "head", + "--datastore-engine", + "postgres", + "--datastore-conn-uri", + "postgres://spicedb-pg:5432/spicedb?sslmode=disable&user=postgres&password=root") + .withStartupCheckStrategy( + new OneShotStartupCheckStrategy().withTimeout(Duration.ofSeconds(5))) + .withNetwork(net); + + var spicedb = createPostgeresSpicedbContainer(net); + + var db = + new PostgreSQLContainer<>(DockerImageName.parse("postgres").withTag("14")) + .withDatabaseName("spicedb") + .withPassword("root") + .withUsername("postgres") + .withNetworkAliases("spicedb-pg") + .withNetwork(net); + + db.start(); + migrator.start(); + spicedb.start(); - var host = environment.getServiceHost(spiceDbServiceName, GRPC_PORT); - var port = environment.getServicePort(spiceDbServiceName, GRPC_PORT); + var host = spicedb.getHost(); + var port = spicedb.getMappedPort(GRPC_PORT); var services = createServices(host, port); return createContext( "postgres backed SpiceDB", services, () -> { quitelyShutdown(services.channel()); - environment.stop(); + migrator.stop(); + spicedb.stop(); + db.stop(); }); } @@ -113,6 +138,29 @@ public List getAdditionalExtensions() { }; } + private GenericContainer createPostgeresSpicedbContainer(Network net) { + + return createSpicedbBaseContainer( + "serve", + "--grpc-preshared-key=%s".formatted(TOKEN), + "--datastore-engine=postgres", + "--datastore-conn-uri=postgres://spicedb-pg:5432/spicedb?sslmode=disable&user=postgres&password=root") + .waitingFor(new LogMessageWaitStrategy().withRegEx(".*\"grpc server started serving\".*")) + .withExposedPorts( + GRPC_PORT, // grpc + 8080, // dashboard + 9090 // metrics + ) + .withNetwork(net); + } + + private GenericContainer createSpicedbBaseContainer(String... args) { + + return new GenericContainer<>(DockerImageName.parse("quay.io/authzed/spicedb:v1.32.0")) + .withCommand(args) + .withLogConsumer(f -> logger.info("spicedb {}: {}", args[0], f.getUtf8String())); + } + public TestServices createServices(String host, int port) { var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); diff --git a/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/UpdateRelationshipMapperTest.java b/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/UpdateRelationshipMapperTest.java new file mode 100644 index 0000000..60807f1 --- /dev/null +++ b/spicedb-binding/src/test/java/com/oviva/spicegen/spicedbbinding/internal/UpdateRelationshipMapperTest.java @@ -0,0 +1,51 @@ +package com.oviva.spicegen.spicedbbinding.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.authzed.api.v1.Core; +import com.oviva.spicegen.api.ObjectRef; +import com.oviva.spicegen.api.UpdateRelationship; +import org.junit.jupiter.api.Test; + +public class UpdateRelationshipMapperTest { + + private static final String TENANT = "tenant"; + private static final String USER = "user"; + private static final String ADMINISTRATOR = "administrator"; + private static final String ID = "9392"; + + private final UpdateRelationshipMapper mapper = new UpdateRelationshipMapper(); + + @Test + public void test_mapper_withUpdateOperation() { + + var resource = ObjectRef.of(TENANT, ID); + var subject = ObjectRef.of(USER, ID); + + var updateRelationship = UpdateRelationship.ofUpdate(resource, ADMINISTRATOR, subject); + var map = mapper.map(updateRelationship); + + assertEquals(map.getOperation(), Core.RelationshipUpdate.Operation.OPERATION_TOUCH); + assertNotNull(map.getRelationship()); + assertEquals(map.getRelationship().getRelation(), ADMINISTRATOR); + assertEquals(map.getRelationship().getResource().getObjectId(), ID); + assertEquals(map.getRelationship().getResource().getObjectType(), TENANT); + } + + @Test + public void test_mapper_withDeleteOperation() { + + var resource = ObjectRef.of(TENANT, ID); + var subject = ObjectRef.of(USER, ID); + + var updateRelationship = UpdateRelationship.ofDelete(resource, ADMINISTRATOR, subject); + var map = mapper.map(updateRelationship); + + assertEquals(map.getOperation(), Core.RelationshipUpdate.Operation.OPERATION_DELETE); + assertNotNull(map.getRelationship()); + assertEquals(map.getRelationship().getRelation(), ADMINISTRATOR); + assertEquals(map.getRelationship().getResource().getObjectId(), ID); + assertEquals(map.getRelationship().getResource().getObjectType(), TENANT); + } +}