Skip to content
Open
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
68 changes: 68 additions & 0 deletions src/main/java/dev/openfga/OpenFga.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.model.ClientCheckRequest;
import dev.openfga.sdk.api.client.model.ClientTupleKey;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.springframework.security.core.context.SecurityContextHolder;

Expand Down Expand Up @@ -69,10 +71,76 @@ public boolean check(String objectType, String objectId, String relation, String
* @see <a href="https://openfga.dev/api/service#/Relationship%20Queries/Check">FGA Check API</a>
*/
public boolean check(String objectType, String objectId, String relation, String userType, String userId) {
return check(objectType, objectId, relation, userType, userId, null, null);
}

/**
* Perform an FGA check, supplying contextual tuples and/or a context object for the evaluation. Returns {@code true}
* if the user has the specified relationship with the object, {@code false} otherwise. The user ID will be obtained
* from the authentication name in the {@link org.springframework.security.core.context.SecurityContext}.<br/><br/>
* Contextual tuples are evaluated as if they existed in the store, and the context object can be used to satisfy
* conditions defined in the authorization model.
*
* @param objectType The object type of the check
* @param objectId The ID of the object to check
* @param relation The required relation between the user and the object
* @param userType The type of the user
* @param contextualTuples The contextual tuples to evaluate as part of the check, may be {@code null}
* @param context The context object used to evaluate conditions in the authorization model, may be {@code null}
* @return true if the user has the required relation to the object, false otherwise
*
* @see <a href="https://openfga.dev/api/service#/Relationship%20Queries/Check">FGA Check API</a>
*/
public boolean check(
String objectType,
String objectId,
String relation,
String userType,
List<ClientTupleKey> contextualTuples,
Object context) {
var authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new IllegalStateException(
"No user provided, and no authentication could be found in the security context");
}
return check(objectType, objectId, relation, userType, authentication.getName(), contextualTuples, context);
}

/**
* Perform an FGA check, supplying contextual tuples and/or a context object for the evaluation. Returns {@code true}
* if the user has the specified relationship with the object, {@code false} otherwise.<br/><br/>Contextual tuples are
* evaluated as if they existed in the store, and the context object can be used to satisfy conditions defined in the
* authorization model.
*
* @param objectType The object type of the check
* @param objectId The ID of the object to check
* @param relation The required relation between the user and the object
* @param userType The type of the user
* @param userId The ID of the user
* @param contextualTuples The contextual tuples to evaluate as part of the check, may be {@code null}
* @param context The context object used to evaluate conditions in the authorization model, may be {@code null}
* @return true if the user has the required relation to the object, false otherwise
*
* @see <a href="https://openfga.dev/api/service#/Relationship%20Queries/Check">FGA Check API</a>
*/
public boolean check(
String objectType,
String objectId,
String relation,
String userType,
String userId,
List<ClientTupleKey> contextualTuples,
Object context) {
var body = new ClientCheckRequest()
.user(String.format("%s:%s", userType, userId))
.relation(relation)
._object(String.format("%s:%s", objectType, objectId));
if (contextualTuples != null) {
body.contextualTuples(contextualTuples);
}
if (context != null) {
body.context(context);
}
try {
return Boolean.TRUE.equals(fgaClient.check(body).get().getAllowed());
} catch (InterruptedException | FgaInvalidParameterException | ExecutionException cause) {
Expand Down
83 changes: 81 additions & 2 deletions src/test/java/dev/openfga/OpenFgaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.model.ClientCheckRequest;
import dev.openfga.sdk.api.client.model.ClientCheckResponse;
import dev.openfga.sdk.api.client.model.ClientTupleKey;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
Expand All @@ -32,6 +36,11 @@ class OpenFgaTest {
@Mock
private ClientCheckResponse mockCheckResponse;

@AfterEach
void clearSecurityContext() {
SecurityContextHolder.clearContext();
}

@Test
void fgaCheckCalled() throws Exception {

Expand All @@ -46,7 +55,6 @@ void fgaCheckCalled() throws Exception {
// then
ArgumentCaptor<ClientCheckRequest> argumentCaptor = ArgumentCaptor.forClass(ClientCheckRequest.class);

verify(mockClient, times(1)).check(any(ClientCheckRequest.class));
verify(mockClient).check(argumentCaptor.capture());

ClientCheckRequest request = argumentCaptor.getValue();
Expand All @@ -72,7 +80,6 @@ void usesPrincipalNameAsUserId() throws Exception {
// then
ArgumentCaptor<ClientCheckRequest> argumentCaptor = ArgumentCaptor.forClass(ClientCheckRequest.class);

verify(mockClient, times(1)).check(any(ClientCheckRequest.class));
verify(mockClient).check(argumentCaptor.capture());

ClientCheckRequest request = argumentCaptor.getValue();
Expand All @@ -94,6 +101,78 @@ void failsWhenNoUserIdSpecifiedAndNotFoundInContext() {
is("No user provided, and no authentication could be found in the security context"));
}

@Test
void fgaCheckCalledWithContextualTuplesAndContext() throws Exception {

// given
OpenFga openFga = new OpenFga(mockClient, exceptionHandler);
when(mockCheckResponseFuture.get()).thenReturn(mockCheckResponse);
when(mockClient.check(any(ClientCheckRequest.class))).thenReturn(mockCheckResponseFuture);

List<ClientTupleKey> contextualTuples = List.of(
new ClientTupleKey().user("user:userId").relation("member")._object("group:marketing"));
Map<String, Object> context = Map.of("current_time", "2023-01-01T00:10:01Z");

// when
openFga.check("document", "docId", "viewer", "user", "userId", contextualTuples, context);

// then
ArgumentCaptor<ClientCheckRequest> argumentCaptor = ArgumentCaptor.forClass(ClientCheckRequest.class);

verify(mockClient).check(argumentCaptor.capture());

ClientCheckRequest request = argumentCaptor.getValue();
assertThat(request.getObject(), is("document:docId"));
assertThat(request.getRelation(), is("viewer"));
assertThat(request.getUser(), is("user:userId"));
assertThat(request.getContextualTuples(), is(contextualTuples));
assertThat(request.getContext(), is(context));
}

@Test
void fgaCheckWithContextualTuplesUsesPrincipalNameAsUserId() throws Exception {
// given
Principal principal = () -> "userId";
Authentication auth = new UsernamePasswordAuthenticationToken(principal, null);
SecurityContextHolder.getContext().setAuthentication(auth);

OpenFga openFga = new OpenFga(mockClient, exceptionHandler);
when(mockCheckResponseFuture.get()).thenReturn(mockCheckResponse);
when(mockClient.check(any(ClientCheckRequest.class))).thenReturn(mockCheckResponseFuture);

List<ClientTupleKey> contextualTuples = List.of(
new ClientTupleKey().user("user:userId").relation("member")._object("group:marketing"));
Map<String, Object> context = Map.of("current_time", "2023-01-01T00:10:01Z");

// when
openFga.check("document", "docId", "viewer", "user", contextualTuples, context);

// then
ArgumentCaptor<ClientCheckRequest> argumentCaptor = ArgumentCaptor.forClass(ClientCheckRequest.class);

verify(mockClient).check(argumentCaptor.capture());

ClientCheckRequest request = argumentCaptor.getValue();
assertThat(request.getObject(), is("document:docId"));
assertThat(request.getRelation(), is("viewer"));
assertThat(request.getUser(), is("user:userId"));
assertThat(request.getContextualTuples(), is(contextualTuples));
assertThat(request.getContext(), is(context));
}

@Test
void failsWhenNoUserIdSpecifiedForContextualCheckAndNotFoundInContext() {
// given
OpenFga openFga = new OpenFga(mockClient, exceptionHandler);

// when/then
IllegalStateException exception = assertThrows(
IllegalStateException.class, () -> openFga.check("document", "docId", "viewer", "user", null, null));
assertThat(
exception.getMessage(),
is("No user provided, and no authentication could be found in the security context"));
}

@Test
void failsWhenCheckHasException() throws Exception {
// given
Expand Down