Skip to content

Commit

Permalink
Add required roles to API endpoints (#21664)
Browse files Browse the repository at this point in the history
* Restore auth poc

* Formatting

* Custom Netty pipeline handler to aid authorization

* Fix handler name

* Cleanup

* Remove cloud code

* Disable API authorization in OSS

* Remove unused dependency

* Add newline

* Add required roles
  • Loading branch information
jdpgrailsdev authored Jan 23, 2023
1 parent d77514d commit b786a18
Show file tree
Hide file tree
Showing 26 changed files with 417 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2022 Airbyte, Inc., all rights reserved.
*/

package io.airbyte.commons.auth;

import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* This enum describes the standard auth levels for a given resource. It currently is only used for
* 2 resources Workspace and Instance (i.e. the entire instance or deployment of Airbyte).
*
* In the context of a workspace, there is a 1:1 mapping.
* <ul>
* <li>OWNER => WORKSPACE OWNER. Superadmin of the instance (typically the person that created it),
* has all the rights on the instance including deleting it.</li>
* <li>ADMIN => WORKSPACE ADMIN. Admin of the instance, can invite other users, update their
* permission and change settings of the instance.</li>
* <li>EDITOR => WORKSPACE EDITOR</li>
* <li>READER => WORKSPACE READER</li>
* <li>AUTHENTICATED_USER => INVALID</li>
* <li>NONE => NONE (does not have access to this resource)</li>
* </ul>
* In the context of the instance, there are currently only 3 levels.
* <ul>
* <li>ADMIN => INSTANCE ADMIN</li>
* <li>AUTHENTICATED_USER => Denotes that all that is required for access is an active Airbyte
* account. This should only ever be used when the associated resource is an INSTANCE. All other
* uses are invalid. It is a special value in the enum to handle a case that only applies to
* instances and no other resources.</li>
* <li>NONE => NONE (not applicable. anyone being checked in our auth stack already has an account
* so by definition they have some access to the instance.)</li>
* </ul>
*/
public enum AuthRole {

OWNER(500, AuthRoleConstants.OWNER),
ADMIN(400, AuthRoleConstants.ADMIN),
EDITOR(300, AuthRoleConstants.EDITOR),
READER(200, AuthRoleConstants.READER),
AUTHENTICATED_USER(100, AuthRoleConstants.AUTHENTICATED_USER), // ONLY USE WITH INSTANCE RESOURCE!
NONE(0, AuthRoleConstants.NONE);

private final int authority;
private final String label;

AuthRole(final int authority, final String label) {
this.authority = authority;
this.label = label;
}

public int getAuthority() {
return authority;
}

public String getLabel() {
return label;
}

/**
* Builds the set of roles based on the provided {@link AuthRole} value.
* <p>
* The generated set of auth roles contains the provided {@link AuthRole} (if not {@code null}) and
* any other authentication roles with a lesser {@link #getAuthority()} value.
* </p>
*
* @param authRole An {@link AuthRole} (may be {@code null}).
* @return The set of {@link AuthRole}s based on the provided {@link AuthRole}.
*/
public static Set<AuthRole> buildAuthRolesSet(final AuthRole authRole) {
final Set<AuthRole> authRoles = new HashSet<>();

if (authRole != null) {
authRoles.add(authRole);
authRoles.addAll(Stream.of(values())
.filter(role -> !NONE.equals(role))
.filter(role -> role.getAuthority() < authRole.getAuthority())
.collect(Collectors.toSet()));
}

// Sort final set by descending authority order
return authRoles.stream()
.sorted(Comparator.comparingInt(AuthRole::getAuthority))
.collect(Collectors.toCollection(LinkedHashSet::new));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2022 Airbyte, Inc., all rights reserved.
*/

package io.airbyte.commons.auth;

/**
* Collection of constants that defines authorization roles.
*/
public final class AuthRoleConstants {

public static final String ADMIN = "ADMIN";
public static final String AUTHENTICATED_USER = "AUTHENTICATED_USER";
public static final String EDITOR = "EDITOR";
public static final String OWNER = "OWNER";
public static final String NONE = "NONE";
public static final String READER = "READER";

private AuthRoleConstants() {}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2022 Airbyte, Inc., all rights reserved.
*/

package io.airbyte.commons.auth;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Set;
import org.junit.jupiter.api.Test;

/**
* Test suite for the {@link AuthRole} enumeration.
*/
class AuthRoleTest {

@Test
void testBuildingAuthRoleSet() {
final Set<AuthRole> ownerResult = AuthRole.buildAuthRolesSet(AuthRole.OWNER);
assertEquals(5, ownerResult.size());
assertEquals(Set.of(AuthRole.OWNER, AuthRole.ADMIN, AuthRole.EDITOR, AuthRole.READER, AuthRole.AUTHENTICATED_USER), ownerResult);

final Set<AuthRole> adminResult = AuthRole.buildAuthRolesSet(AuthRole.ADMIN);
assertEquals(4, adminResult.size());
assertEquals(Set.of(AuthRole.ADMIN, AuthRole.EDITOR, AuthRole.READER, AuthRole.AUTHENTICATED_USER), adminResult);

final Set<AuthRole> editorResult = AuthRole.buildAuthRolesSet(AuthRole.EDITOR);
assertEquals(3, editorResult.size());
assertEquals(Set.of(AuthRole.EDITOR, AuthRole.READER, AuthRole.AUTHENTICATED_USER), editorResult);

final Set<AuthRole> readerResult = AuthRole.buildAuthRolesSet(AuthRole.READER);
assertEquals(2, readerResult.size());
assertEquals(Set.of(AuthRole.READER, AuthRole.AUTHENTICATED_USER), readerResult);

final Set<AuthRole> authenticatedUserResult = AuthRole.buildAuthRolesSet(AuthRole.AUTHENTICATED_USER);
assertEquals(1, authenticatedUserResult.size());
assertEquals(Set.of(AuthRole.AUTHENTICATED_USER), authenticatedUserResult);

final Set<AuthRole> noneResult = AuthRole.buildAuthRolesSet(AuthRole.NONE);
assertEquals(1, noneResult.size());
assertEquals(Set.of(AuthRole.NONE), noneResult);

final Set<AuthRole> nullResult = AuthRole.buildAuthRolesSet(null);
assertEquals(0, nullResult.size());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package io.airbyte.server.apis;

import static io.airbyte.commons.auth.AuthRoleConstants.ADMIN;

import io.airbyte.api.generated.AttemptApi;
import io.airbyte.api.model.generated.InternalOperationResult;
import io.airbyte.api.model.generated.SaveStatsRequestBody;
Expand All @@ -14,10 +16,13 @@
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;

@Controller("/api/v1/attempt/")
@Requires(property = "airbyte.deployment-mode",
value = "OSS")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class AttemptApiController implements AttemptApi {

private final AttemptHandler attemptHandler;
Expand All @@ -36,6 +41,7 @@ public InternalOperationResult saveStats(final SaveStatsRequestBody requestBody)
@Override
@Post(uri = "/set_workflow_in_attempt",
processes = MediaType.APPLICATION_JSON)
@Secured({ADMIN})
public InternalOperationResult setWorkflowInAttempt(@Body final SetWorkflowInAttemptRequestBody requestBody) {
return ApiHelper.execute(() -> attemptHandler.setWorkflowInAttempt(requestBody));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

package io.airbyte.server.apis;

import static io.airbyte.commons.auth.AuthRoleConstants.EDITOR;
import static io.airbyte.commons.auth.AuthRoleConstants.READER;

import io.airbyte.api.generated.ConnectionApi;
import io.airbyte.api.model.generated.ConnectionCreate;
import io.airbyte.api.model.generated.ConnectionIdRequestBody;
Expand All @@ -21,11 +24,14 @@
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;

@Controller("/api/v1/connections")
@Context()
@Requires(property = "airbyte.deployment-mode",
value = "OSS")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class ConnectionApiController implements ConnectionApi {

private final ConnectionsHandler connectionsHandler;
Expand All @@ -42,24 +48,28 @@ public ConnectionApiController(final ConnectionsHandler connectionsHandler,

@Override
@Post(uri = "/create")
@Secured({EDITOR})
public ConnectionRead createConnection(@Body final ConnectionCreate connectionCreate) {
return ApiHelper.execute(() -> connectionsHandler.createConnection(connectionCreate));
}

@Override
@Post(uri = "/update")
@Secured({EDITOR})
public ConnectionRead updateConnection(@Body final ConnectionUpdate connectionUpdate) {
return ApiHelper.execute(() -> connectionsHandler.updateConnection(connectionUpdate));
}

@Override
@Post(uri = "/list")
@Secured({READER})
public ConnectionReadList listConnectionsForWorkspace(@Body final WorkspaceIdRequestBody workspaceIdRequestBody) {
return ApiHelper.execute(() -> connectionsHandler.listConnectionsForWorkspace(workspaceIdRequestBody));
}

@Override
@Post(uri = "/list_all")
@Secured({READER})
public ConnectionReadList listAllConnectionsForWorkspace(@Body final WorkspaceIdRequestBody workspaceIdRequestBody) {
return ApiHelper.execute(() -> connectionsHandler.listAllConnectionsForWorkspace(workspaceIdRequestBody));
}
Expand All @@ -72,12 +82,14 @@ public ConnectionReadList searchConnections(@Body final ConnectionSearch connect

@Override
@Post(uri = "/get")
@Secured({READER})
public ConnectionRead getConnection(@Body final ConnectionIdRequestBody connectionIdRequestBody) {
return ApiHelper.execute(() -> connectionsHandler.getConnection(connectionIdRequestBody.getConnectionId()));
}

@Override
@Post(uri = "/delete")
@Secured({EDITOR})
public void deleteConnection(@Body final ConnectionIdRequestBody connectionIdRequestBody) {
ApiHelper.execute(() -> {
operationsHandler.deleteOperationsForConnection(connectionIdRequestBody);
Expand All @@ -88,12 +100,14 @@ public void deleteConnection(@Body final ConnectionIdRequestBody connectionIdReq

@Override
@Post(uri = "/sync")
@Secured({EDITOR})
public JobInfoRead syncConnection(@Body final ConnectionIdRequestBody connectionIdRequestBody) {
return ApiHelper.execute(() -> schedulerHandler.syncConnection(connectionIdRequestBody));
}

@Override
@Post(uri = "/reset")
@Secured({EDITOR})
public JobInfoRead resetConnection(@Body final ConnectionIdRequestBody connectionIdRequestBody) {
return ApiHelper.execute(() -> schedulerHandler.resetConnection(connectionIdRequestBody));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

package io.airbyte.server.apis;

import static io.airbyte.commons.auth.AuthRoleConstants.EDITOR;
import static io.airbyte.commons.auth.AuthRoleConstants.READER;

import io.airbyte.api.generated.DestinationApi;
import io.airbyte.api.model.generated.CheckConnectionRead;
import io.airbyte.api.model.generated.DestinationCloneRequestBody;
Expand All @@ -20,24 +23,32 @@
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import lombok.AllArgsConstructor;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;

@Controller("/api/v1/destinations")
@Requires(property = "airbyte.deployment-mode",
value = "OSS")
@AllArgsConstructor
@Secured(SecurityRule.IS_AUTHENTICATED)
public class DestinationApiController implements DestinationApi {

private final DestinationHandler destinationHandler;
private final SchedulerHandler schedulerHandler;

public DestinationApiController(final DestinationHandler destinationHandler, final SchedulerHandler schedulerHandler) {
this.destinationHandler = destinationHandler;
this.schedulerHandler = schedulerHandler;
}

@Post(uri = "/check_connection")
@Secured({EDITOR})
@Override
public CheckConnectionRead checkConnectionToDestination(@Body final DestinationIdRequestBody destinationIdRequestBody) {
return ApiHelper.execute(() -> schedulerHandler.checkDestinationConnectionFromDestinationId(destinationIdRequestBody));
}

@Post(uri = "/check_connection_for_update")
@Secured({EDITOR})
@Override
public CheckConnectionRead checkConnectionToDestinationForUpdate(@Body final DestinationUpdate destinationUpdate) {
return ApiHelper.execute(() -> schedulerHandler.checkDestinationConnectionFromDestinationIdForUpdate(destinationUpdate));
Expand All @@ -50,12 +61,14 @@ public DestinationRead cloneDestination(@Body final DestinationCloneRequestBody
}

@Post(uri = "/create")
@Secured({EDITOR})
@Override
public DestinationRead createDestination(@Body final DestinationCreate destinationCreate) {
return ApiHelper.execute(() -> destinationHandler.createDestination(destinationCreate));
}

@Post(uri = "/delete")
@Secured({EDITOR})
@Override
public void deleteDestination(@Body final DestinationIdRequestBody destinationIdRequestBody) {
ApiHelper.execute(() -> {
Expand All @@ -65,12 +78,14 @@ public void deleteDestination(@Body final DestinationIdRequestBody destinationId
}

@Post(uri = "/get")
@Secured({READER})
@Override
public DestinationRead getDestination(@Body final DestinationIdRequestBody destinationIdRequestBody) {
return ApiHelper.execute(() -> destinationHandler.getDestination(destinationIdRequestBody));
}

@Post(uri = "/list")
@Secured({READER})
@Override
public DestinationReadList listDestinationsForWorkspace(@Body final WorkspaceIdRequestBody workspaceIdRequestBody) {
return ApiHelper.execute(() -> destinationHandler.listDestinationsForWorkspace(workspaceIdRequestBody));
Expand All @@ -83,6 +98,7 @@ public DestinationReadList searchDestinations(@Body final DestinationSearch dest
}

@Post(uri = "/update")
@Secured({EDITOR})
@Override
public DestinationRead updateDestination(@Body final DestinationUpdate destinationUpdate) {
return ApiHelper.execute(() -> destinationHandler.updateDestination(destinationUpdate));
Expand Down
Loading

0 comments on commit b786a18

Please sign in to comment.