Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/main/java/app/quickcase/sdk/spring/acl/Acl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package app.quickcase.sdk.spring.acl;

import java.util.Arrays;
import java.util.Map;
import java.util.Set;

import lombok.NonNull;

/**
* Defines binary role-based ACL model and implement permission evaluation.
*/
public final class Acl {
public static int CREATE = 0b1000;
public static int READ = 0b0100;
public static int UPDATE = 0b0010;
public static int DELETE = 0b0001;
public static int CRUD = CREATE | READ | UPDATE | DELETE;

public static Map<Character, Integer> LETTERS = Map.of(
'C', CREATE,
'R', READ,
'U', UPDATE,
'D', DELETE
);

public static int fromFlags(boolean create, boolean read, boolean update, boolean delete) {
return (create ? CREATE : 0) | (read ? READ : 0) | (update ? UPDATE : 0) | (delete ? DELETE : 0);
}

public static int fromString(String permission) {
return permission.chars()
.reduce(0, (acc, letter) -> LETTERS.getOrDefault((char) letter, 0) | acc);
}

private final Map<String, Integer> acl;

public Acl(Map<String, Integer> acl) {
this.acl = acl;
}

public boolean check(@NonNull Set<String> roles, int verb) {
return roles.stream()
.anyMatch(role -> (acl.getOrDefault(role, 0) & verb) > 0);
}

public boolean checkAny(@NonNull Set<String> roles, int... verbs) {
return check(roles, combine(verbs));
}

public boolean checkAll(@NonNull Set<String> roles, int... verbs) {
var combinedVerbs = combine(verbs);
var cumulatedPermissions = roles.stream().reduce(0, (acc, role) -> (acl.getOrDefault(role, 0) | acc), (a, b) -> a | b);
return (combinedVerbs & cumulatedPermissions) == combinedVerbs;
}

private static int combine(int[] verbs) {
return Arrays.stream(verbs).reduce(0, (acc, verb) -> acc | verb);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public record DataField(
@Singular Map<String, DataField> members,
Validation validation,
Field.Display display,
@NonNull @Singular("acl") Map<String, Short> acl,
@NonNull @Singular("acl") Map<String, Integer> acl,
@NonNull String classification
) implements Field {
@Builder
Expand Down
165 changes: 165 additions & 0 deletions src/test/java/app/quickcase/sdk/spring/acl/AclTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package app.quickcase.sdk.spring.acl;

import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import static app.quickcase.sdk.spring.acl.Acl.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

class AclTest {

@Nested
class FromFlags {
@ParameterizedTest
@MethodSource
@DisplayName("should convert permission flags to binary")
void shouldConvertPermissionFlagsToBinary(boolean create, boolean read, boolean update, boolean delete, int expected) {
assertThat(Acl.fromFlags(create, read, update, delete), is(expected));
}

static Stream<Arguments> shouldConvertPermissionFlagsToBinary() {
return Stream.of(
Arguments.of(true, false, false, false, CREATE),
Arguments.of(false, true, false, false, READ),
Arguments.of(false, false, true, false, UPDATE),
Arguments.of(false, false, false, true, DELETE),
Arguments.of(true, true, false, false, CREATE | READ),
Arguments.of(false, true, true, false, READ | UPDATE),
Arguments.of(true, true, true, true, CRUD)
);
}
}

@Nested
class FromString {
@ParameterizedTest
@MethodSource
@DisplayName("should convert permission string to binary")
void shouldConvertPermissionStringToBinary(String permission, int expected) {
assertThat(Acl.fromString(permission), is(expected));
}

static Stream<Arguments> shouldConvertPermissionStringToBinary() {
return Stream.of(
Arguments.of("C", CREATE),
Arguments.of("R", READ),
Arguments.of("U", UPDATE),
Arguments.of("D", DELETE),
Arguments.of("CR", CREATE | READ),
Arguments.of("RU", READ | UPDATE),
Arguments.of("CRUD", CRUD),
Arguments.of("DUCR", CRUD),
Arguments.of("", 0)
);
}
}

@Nested
class Check {
@Test
@DisplayName("should be true when any role is granted requested verb")
void shouldBeTrueWhenAnyRoleGranted() {
var acl = new Acl(Map.of(
"role-1", CREATE,
"role-2", CREATE | READ,
"role-3", DELETE
));

assertThat(
acl.check(Set.of("role-2", "role-3", "role-4"), READ),
is(true)
);
}

@Test
@DisplayName("should be false when no role is granted requested verb")
void shouldBefalseWhenNoRoleGranted() {
var acl = new Acl(Map.of(
"role-1", CREATE | UPDATE,
"role-2", CREATE | READ,
"role-3", DELETE
));

assertThat(
acl.check(Set.of("role-2", "role-3", "role-4"), UPDATE),
is(false)
);
}
}

@Nested
class CheckAny {
@Test
@DisplayName("should be true when any role is granted any of requested verbs")
void shouldBeTrueWhenAnyRoleGranted() {
var acl = new Acl(Map.of(
"role-1", CREATE,
"role-2", CREATE | READ,
"role-3", DELETE
));

assertThat(
acl.checkAny(Set.of("role-2", "role-3", "role-4"), READ, UPDATE),
is(true)
);
}

@Test
@DisplayName("should be false when no role is any of requested verbs")
void shouldBefalseWhenNoRoleGranted() {
var acl = new Acl(Map.of(
"role-1", CREATE | UPDATE,
"role-2", CREATE | READ,
"role-3", DELETE
));

assertThat(
acl.checkAny(Set.of("role-2", "role-4"), UPDATE, DELETE),
is(false)
);
}
}

@Nested
class CheckAll {
@Test
@DisplayName("should be true when combined roles are granted all of requested verbs")
void shouldBeTrueWhenAnyRoleGranted() {
var acl = new Acl(Map.of(
"role-1", CREATE,
"role-2", CREATE | READ,
"role-3", DELETE
));

assertThat(
acl.checkAll(Set.of("role-2", "role-3", "role-4"), CREATE, READ, DELETE),
is(true)
);
}

@Test
@DisplayName("should be false when no role is any of requested verbs")
void shouldBefalseWhenNoRoleGranted() {
var acl = new Acl(Map.of(
"role-1", CREATE | UPDATE,
"role-2", CREATE | READ,
"role-3", DELETE
));

assertThat(
acl.checkAll(Set.of("role-1", "role-2"), CREATE, READ, DELETE),
is(false)
);
}
}

}