Skip to content

Commit

Permalink
RBAC: Several small fixes to unblock Reader role release (#11355)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmossman committed Feb 22, 2024
1 parent 7add3ee commit 79a9585
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -525,8 +525,12 @@ private OrganizationUserReadList buildOrganizationUserReadList(final List<UserPe
}

private WorkspaceUserAccessInfoReadList buildWorkspaceUserAccessInfoReadList(final List<WorkspaceUserAccessInfo> accessInfos) {
return new WorkspaceUserAccessInfoReadList()
.usersWithAccess(accessInfos.stream().map(this::buildWorkspaceUserAccessInfoRead).collect(Collectors.toList()));
// we exclude the default user from this list because we don't want to expose it in the UI
return new WorkspaceUserAccessInfoReadList().usersWithAccess(accessInfos
.stream()
.filter(accessInfo -> !accessInfo.getUserId().equals(DEFAULT_USER_ID))
.map(this::buildWorkspaceUserAccessInfoRead)
.collect(Collectors.toList()));
}

private WorkspaceUserAccessInfoRead buildWorkspaceUserAccessInfoRead(final WorkspaceUserAccessInfo accessInfo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ void testListInstanceAdminUser() throws Exception {
void testListAccessInfoByWorkspaceId() throws Exception {
final UUID workspaceId = UUID.randomUUID();
when(userPersistence.listWorkspaceUserAccessInfo(workspaceId)).thenReturn(List.of(
new WorkspaceUserAccessInfo()
.withUserId(DEFAULT_USER_ID), // expect the default user to be filtered out.
new WorkspaceUserAccessInfo()
.withUserId(USER_ID)
.withUserName(USER_NAME)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

package io.airbyte.server.apis;

import static io.airbyte.commons.auth.AuthRoleConstants.AUTHENTICATED_USER;
import static io.airbyte.commons.auth.AuthRoleConstants.ORGANIZATION_EDITOR;
import static io.airbyte.commons.auth.AuthRoleConstants.WORKSPACE_EDITOR;

Expand Down Expand Up @@ -32,15 +31,15 @@ public SchedulerApiController(final SchedulerHandler schedulerHandler) {
}

@Post("/destinations/check_connection")
@Secured({AUTHENTICATED_USER})
@Secured({WORKSPACE_EDITOR, ORGANIZATION_EDITOR})
@ExecuteOn(AirbyteTaskExecutors.SCHEDULER)
@Override
public CheckConnectionRead executeDestinationCheckConnection(final DestinationCoreConfig destinationCoreConfig) {
return ApiHelper.execute(() -> schedulerHandler.checkDestinationConnectionFromDestinationCreate(destinationCoreConfig));
}

@Post("/sources/check_connection")
@Secured({AUTHENTICATED_USER})
@Secured({WORKSPACE_EDITOR, ORGANIZATION_EDITOR})
@ExecuteOn(AirbyteTaskExecutors.SCHEDULER)
@Override
public CheckConnectionRead executeSourceCheckConnection(final SourceCoreConfig sourceCoreConfig) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
import io.airbyte.api.model.generated.ListResourcesForWorkspacesRequestBody;
import io.airbyte.api.model.generated.ListWorkspacesByUserRequestBody;
import io.airbyte.api.model.generated.ListWorkspacesInOrganizationRequestBody;
import io.airbyte.api.model.generated.PermissionCheckRead.StatusEnum;
import io.airbyte.api.model.generated.PermissionCheckRequest;
import io.airbyte.api.model.generated.PermissionType;
import io.airbyte.api.model.generated.SlugRequestBody;
import io.airbyte.api.model.generated.WorkspaceCreate;
import io.airbyte.api.model.generated.WorkspaceCreateWithId;
Expand All @@ -29,8 +32,11 @@
import io.airbyte.api.model.generated.WorkspaceUpdate;
import io.airbyte.api.model.generated.WorkspaceUpdateName;
import io.airbyte.api.model.generated.WorkspaceUpdateOrganization;
import io.airbyte.api.server.problems.ForbiddenProblem;
import io.airbyte.commons.server.handlers.PermissionHandler;
import io.airbyte.commons.server.handlers.WorkspacesHandler;
import io.airbyte.commons.server.scheduling.AirbyteTaskExecutors;
import io.airbyte.commons.server.support.CurrentUserService;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
Expand All @@ -46,23 +52,58 @@
public class WorkspaceApiController implements WorkspaceApi {

private final WorkspacesHandler workspacesHandler;
private final PermissionHandler permissionHandler;
private final CurrentUserService currentUserService;

public WorkspaceApiController(final WorkspacesHandler workspacesHandler) {
public WorkspaceApiController(final WorkspacesHandler workspacesHandler,
final PermissionHandler permissionHandler,
final CurrentUserService currentUserService) {
this.workspacesHandler = workspacesHandler;
this.permissionHandler = permissionHandler;
this.currentUserService = currentUserService;
}

@Post("/create")
@Secured({AUTHENTICATED_USER})
@Override
public WorkspaceRead createWorkspace(@Body final WorkspaceCreate workspaceCreate) {
return ApiHelper.execute(() -> workspacesHandler.createWorkspace(workspaceCreate));
return ApiHelper.execute(() -> {
// Verify that the user has permission to create a workspace in an organization,
// need to be at least an organization editor to do so.
if (workspaceCreate.getOrganizationId() != null) {
final StatusEnum permissionCheckStatus = permissionHandler.checkPermissions(new PermissionCheckRequest()
.userId(currentUserService.getCurrentUser().getUserId())
.permissionType(PermissionType.ORGANIZATION_EDITOR)
.organizationId(workspaceCreate.getOrganizationId()))
.getStatus();
if (!permissionCheckStatus.equals(StatusEnum.SUCCEEDED)) {
throw new ForbiddenProblem("User does not have permission to create a workspace in organization " + workspaceCreate.getOrganizationId());
}
}
return workspacesHandler.createWorkspace(workspaceCreate);
});
}

@Post("/create_if_not_exist")
@Secured({AUTHENTICATED_USER})
@Override
public WorkspaceRead createWorkspaceIfNotExist(@Body final WorkspaceCreateWithId workspaceCreateWithId) {
return ApiHelper.execute(() -> workspacesHandler.createWorkspaceIfNotExist(workspaceCreateWithId));
return ApiHelper.execute(() -> {
// Verify that the user has permission to create a workspace in an organization,
// need to be at least an organization editor to do so.
if (workspaceCreateWithId.getOrganizationId() != null) {
final StatusEnum permissionCheckStatus = permissionHandler.checkPermissions(new PermissionCheckRequest()
.userId(currentUserService.getCurrentUser().getUserId())
.permissionType(PermissionType.ORGANIZATION_EDITOR)
.organizationId(workspaceCreateWithId.getOrganizationId()))
.getStatus();
if (!permissionCheckStatus.equals(StatusEnum.SUCCEEDED)) {
throw new ForbiddenProblem(
"User does not have permission to create a workspace in organization " + workspaceCreateWithId.getOrganizationId());
}
}
return workspacesHandler.createWorkspaceIfNotExist(workspaceCreateWithId);
});
}

@Post("/delete")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.airbyte.commons.server.handlers.WebBackendGeographiesHandler;
import io.airbyte.commons.server.handlers.WorkspacesHandler;
import io.airbyte.commons.server.scheduler.SynchronousSchedulerClient;
import io.airbyte.commons.server.support.CurrentUserService;
import io.airbyte.commons.server.validation.ActorDefinitionAccessValidator;
import io.airbyte.commons.temporal.TemporalClient;
import io.airbyte.db.Database;
Expand Down Expand Up @@ -327,6 +328,14 @@ SecurityService mmSecurityService() {
return Mockito.mock(SecurityService.class);
}

CurrentUserService currentUserService = Mockito.mock(CurrentUserService.class);

@MockBean(CurrentUserService.class)
@Replaces(CurrentUserService.class)
CurrentUserService mmCurrentUserService() {
return currentUserService;
}

@MockBean(JobNotifier.class)
@Replaces(JobNotifier.class)
JobNotifier mmJobNotifier() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@

package io.airbyte.server.apis;

import io.airbyte.api.model.generated.PermissionCheckRead;
import io.airbyte.api.model.generated.PermissionCheckRead.StatusEnum;
import io.airbyte.api.model.generated.SourceDefinitionIdRequestBody;
import io.airbyte.api.model.generated.SourceIdRequestBody;
import io.airbyte.api.model.generated.WorkspaceCreate;
import io.airbyte.api.model.generated.WorkspaceCreateWithId;
import io.airbyte.api.model.generated.WorkspaceRead;
import io.airbyte.api.model.generated.WorkspaceReadList;
import io.airbyte.api.model.generated.WorkspaceUpdateOrganization;
import io.airbyte.commons.json.Jsons;
import io.airbyte.config.User;
import io.airbyte.config.persistence.ConfigNotFoundException;
import io.airbyte.validation.json.JsonValidationException;
import io.micronaut.context.annotation.Requires;
Expand All @@ -18,6 +23,7 @@
import io.micronaut.http.HttpStatus;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import java.io.IOException;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

Expand All @@ -28,12 +34,60 @@ class WorkspaceApiTest extends BaseControllerTest {

@Test
void testCreateWorkspace() throws JsonValidationException, IOException, ConfigNotFoundException {
Mockito.when(permissionHandler.checkPermissions(Mockito.any()))
.thenReturn(new PermissionCheckRead().status(StatusEnum.SUCCEEDED)) // first call with an orgId succeeds
.thenReturn(new PermissionCheckRead().status(StatusEnum.FAILED)); // second call with an orgId fails

Mockito.when(workspacesHandler.createWorkspace(Mockito.any()))
.thenReturn(new WorkspaceRead());

Mockito.when(currentUserService.getCurrentUser()).thenReturn(new User());

final String path = "/api/v1/workspaces/create";

// no org id, expect 200
testEndpointStatus(
HttpRequest.POST(path, Jsons.serialize(new SourceIdRequestBody())),
HttpRequest.POST(path, Jsons.serialize(new WorkspaceCreate())),
HttpStatus.OK);

// org id present, permission check succeeds, expect 200
testEndpointStatus(
HttpRequest.POST(path, Jsons.serialize(new WorkspaceCreate().organizationId(UUID.randomUUID()))),
HttpStatus.OK);

// org id present, permission check fails, expect 403
testErrorEndpointStatus(
HttpRequest.POST(path, Jsons.serialize(new WorkspaceCreate().organizationId(UUID.randomUUID()))),
HttpStatus.FORBIDDEN);
}

@Test
void testCreateWorkspaceIfNotExist() throws JsonValidationException, IOException, ConfigNotFoundException {
Mockito.when(permissionHandler.checkPermissions(Mockito.any()))
.thenReturn(new PermissionCheckRead().status(StatusEnum.SUCCEEDED)) // first call with an orgId succeeds
.thenReturn(new PermissionCheckRead().status(StatusEnum.FAILED)); // second call with an orgId fails

Mockito.when(workspacesHandler.createWorkspaceIfNotExist(Mockito.any()))
.thenReturn(new WorkspaceRead());

Mockito.when(currentUserService.getCurrentUser()).thenReturn(new User());

final String path = "/api/v1/workspaces/create_if_not_exist";

// no org id, expect 200
testEndpointStatus(
HttpRequest.POST(path, Jsons.serialize(new WorkspaceCreateWithId())),
HttpStatus.OK);

// org id present, permission check succeeds, expect 200
testEndpointStatus(
HttpRequest.POST(path, Jsons.serialize(new WorkspaceCreateWithId().organizationId(UUID.randomUUID()))),
HttpStatus.OK);

// org id present, permission check fails, expect 403
testErrorEndpointStatus(
HttpRequest.POST(path, Jsons.serialize(new WorkspaceCreateWithId().organizationId(UUID.randomUUID()))),
HttpStatus.FORBIDDEN);
}

@Test
Expand Down

0 comments on commit 79a9585

Please sign in to comment.