Skip to content

Commit

Permalink
Cloud's listWorkspacesByUser and listUsersByWorkspace leverage Organi…
Browse files Browse the repository at this point in the history
…zation-level permissions (#9322)
  • Loading branch information
pmossman committed Oct 18, 2023
1 parent 9da7c92 commit 62cb0c0
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 97 deletions.
8 changes: 5 additions & 3 deletions airbyte-api/src/main/openapi/cloud-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1046,8 +1046,6 @@ components:
- isDefaultWorkspace
- status
- email
- permissionId
- permissionType
- workspaceId
properties:
name:
Expand All @@ -1063,6 +1061,7 @@ components:
type: string
format: email
permissionId:
description: (Deprecated) Permission ID from the Cloud database.
type: string
format: uuid
permissionType:
Expand Down Expand Up @@ -1680,7 +1679,10 @@ components:
$ref: "#/components/schemas/WorkspaceId"
PermissionType:
type: string
description: Describes what actions/endpoints the permission entitles to
description: |
(Deprecated) Describes what actions/endpoints the permission entitles to. In the process
of migration Permissions to Config API/DB, so these Cloud-specific permissions are
phasing out.
enum:
- instance_admin # Permission to do anything (all permissions below)
- workspace_owner # Permission to own a workspace (and being billed for it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ public WorkspaceReadList listWorkspacesByUser(final ListWorkspacesByUserRequestB
.collect(Collectors.toList());
} else {
standardWorkspaces = workspacePersistence
.listWorkspacesByUserId(request.getUserId(), keyword)
.listActiveWorkspacesByUserId(request.getUserId(), keyword)
.stream()
.map(WorkspacesHandler::buildWorkspaceRead)
.collect(Collectors.toList());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2023 Airbyte, Inc., all rights reserved.
*/

package io.airbyte.config.persistence;

import io.airbyte.config.Permission.PermissionType;
import io.airbyte.config.helpers.PermissionHelper;

public class PermissionPersistenceHelper {

/**
* Get an array of the Jooq enum values for the permission types that grant the target permission
* type. Used for `ANY(?)` clauses in SQL queries.
*/
public static io.airbyte.db.instance.configs.jooq.generated.enums.PermissionType[] getGrantingPermissionTypeArray(final PermissionType targetPermissionType) {
return PermissionHelper.getPermissionTypesThatGrantTargetPermission(targetPermissionType)
.stream()
.map(PermissionPersistenceHelper::convertConfigPermissionTypeToJooqPermissionType)
.toList()
.toArray(new io.airbyte.db.instance.configs.jooq.generated.enums.PermissionType[0]);
}

private static io.airbyte.db.instance.configs.jooq.generated.enums.PermissionType convertConfigPermissionTypeToJooqPermissionType(final PermissionType permissionType) {
// workspace owner is deprecated and doesn't exist in OSS jooq. it is equivalent to workspace admin.
if (permissionType.equals(PermissionType.WORKSPACE_OWNER)) {
return io.airbyte.db.instance.configs.jooq.generated.enums.PermissionType.workspace_admin;
}

return io.airbyte.db.instance.configs.jooq.generated.enums.PermissionType.valueOf(permissionType.value());
}

/**
* This query lists all active workspaces that a particular user has the indicated permissions for.
* The query is parameterized by a user id, a permission type array, and a keyword search string.
* <p>
* Note: The permission type array should include the valid set of permission types that can be used
* to infer workspace access.
* <p>
* For instance, if the passed-in permission type array contains `organization_admin` and
* `workspace_admin`, then the query will return all workspaces that belong to an organization that
* the user has `organization_admin` permissions for, as well as all workspaces that the user has
* `workspace_admin` permissions for.
*/
public final static String LIST_ACTIVE_WORKSPACES_BY_USER_ID_AND_PERMISSION_TYPES_QUERY =
"WITH "
+ " userOrgs AS ("
+ " SELECT organization_id FROM permission WHERE user_id = {0} AND permission_type = ANY({1}::permission_type[])"
+ " ),"
+ " userWorkspaces AS ("
+ " SELECT workspace.id AS workspace_id FROM userOrgs JOIN workspace"
+ " ON workspace.organization_id = userOrgs.organization_id"
+ " UNION"
+ " SELECT workspace_id FROM permission WHERE user_id = {0} AND permission_type = ANY({1}::permission_type[])"
+ " )"
+ " SELECT * from workspace"
+ " WHERE workspace.id IN (SELECT workspace_id from userWorkspaces)"
+ " AND name ILIKE {2}"
+ " AND tombstone = false"
+ " ORDER BY name ASC";

/**
* This query lists all users that can access the particular workspace through possession of the
* indicated permissions. The query is parameterized by a workspace id and a permission type array.
* <p>
* Note: The permission type array should include the valid set of permission types that can be used
* to infer workspace access.
* <p>
* For instance, if the passed-in permission type array contains `organization_admin` and
* `workspace_admin`, then the query will return all users that can access the indicated workspace
* through possession of either of those two permission_types.
*/
public final static String LIST_USERS_BY_WORKSPACE_ID_AND_PERMISSION_TYPES_QUERY =
"WITH "
+ " orgWorkspaces AS ("
+ " SELECT organization_id FROM workspace WHERE workspace.id = {0}"
+ " ),"
+ " usersInOrgWithPerm AS ("
+ " SELECT permission.user_id FROM permission"
+ " JOIN orgWorkspaces ON permission.organization_id = orgWorkspaces.organization_id"
+ " WHERE permission_type = ANY({1}::permission_type[])"
+ " ),"
+ " usersInWorkspaceWithPerm AS ("
+ " SELECT user_id FROM permission WHERE workspace_id = {0} "
+ " AND permission_type = ANY({1}::permission_type[])"
+ " )"
+ " SELECT * from \"user\""
+ " WHERE \"user\".id IN (SELECT user_id FROM usersInOrgWithPerm UNION SELECT user_id FROM usersInWorkspaceWithPerm)"
+ " ORDER BY name ASC";

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
import static org.jooq.impl.DSL.select;

import io.airbyte.commons.enums.Enums;
import io.airbyte.config.Permission.PermissionType;
import io.airbyte.config.User;
import io.airbyte.db.Database;
import io.airbyte.db.ExceptionWrappingDatabase;
import io.airbyte.db.instance.configs.jooq.generated.enums.AuthProvider;
import io.airbyte.db.instance.configs.jooq.generated.enums.Status;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -190,4 +192,18 @@ public Optional<User> getDefaultUser() throws IOException {
return getUser(DEFAULT_USER_ID);
}

/**
* Get all users that have read access to the specified workspace.
*/
public List<User> getUsersWithWorkspaceAccess(final UUID workspaceId) throws IOException {
return database
.query(ctx -> ctx.fetch(
PermissionPersistenceHelper.LIST_USERS_BY_WORKSPACE_ID_AND_PERMISSION_TYPES_QUERY,
workspaceId,
PermissionPersistenceHelper.getGrantingPermissionTypeArray(PermissionType.WORKSPACE_READER)))
.stream()
.map(this::createUserFromRecord)
.toList();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import io.airbyte.config.Permission.PermissionType;
import io.airbyte.config.StandardWorkspace;
import io.airbyte.config.helpers.PermissionHelper;
import io.airbyte.config.persistence.ConfigRepository.ResourcesByOrganizationQueryPaginated;
import io.airbyte.config.persistence.ConfigRepository.ResourcesByUserQueryPaginated;
import io.airbyte.db.Database;
Expand Down Expand Up @@ -109,32 +108,6 @@ public List<StandardWorkspace> listWorkspacesByOrganizationId(UUID organizationI
.toList();
}

/**
* This query lists all workspaces that a particular user has the indicated permissions for. The
* query is parameterized by a user id, a permission type array, and a keyword search string.
* <p>
* Note: The permission type array should include the valid set of permission types that can be used
* to infer workspace access.
* <p>
* For instance, if the passed-in permission type array contains `organization_admin` and
* `workspace_admin`, then the query will return all workspaces that belong to an organization that
* the user has `organization_admin` permissions for, as well as all workspaces that the user has
* `workspace_admin` permissions for.
*/
private final String listWorkspacesByUserIdAndPermissionTypeBasicQuery =
"WITH userOrgs AS (SELECT organization_id FROM permission WHERE user_id = {0} AND permission_type = ANY({1}::permission_type[])),"
+ " userWorkspaces AS ("
+ " SELECT workspace.id AS workspace_id FROM userOrgs JOIN workspace"
+ " ON workspace.organization_id = userOrgs.organization_id"
+ " UNION"
+ " SELECT workspace_id FROM permission WHERE user_id = {0} AND permission_type = ANY({1}::permission_type[])"
+ " )"
+ " SELECT * from workspace"
+ " WHERE workspace.id IN (SELECT workspace_id from userWorkspaces)"
+ " AND name ILIKE {2}"
+ " AND tombstone = false"
+ " ORDER BY name ASC";

/**
* Get search keyword with flexible matching.
*/
Expand All @@ -147,36 +120,18 @@ private String getSearchKeyword(Optional<String> keyword) {
}

/**
* Get an array of the Jooq enum values for the permission types that grant the target permission
* type. Used for `ANY(?)` clauses in SQL queries.
*/
private io.airbyte.db.instance.configs.jooq.generated.enums.PermissionType[] getGrantingPermissionTypeArray(final PermissionType targetPermissionType) {
return PermissionHelper.getPermissionTypesThatGrantTargetPermission(targetPermissionType)
.stream()
.map(this::convertConfigPermissionTypeToJooqPermissionType)
.toList()
.toArray(new io.airbyte.db.instance.configs.jooq.generated.enums.PermissionType[0]);
}

private io.airbyte.db.instance.configs.jooq.generated.enums.PermissionType convertConfigPermissionTypeToJooqPermissionType(final PermissionType permissionType) {
// workspace owner is deprecated and doesn't exist in OSS jooq. it is equivalent to workspace admin.
if (permissionType.equals(PermissionType.WORKSPACE_OWNER)) {
return io.airbyte.db.instance.configs.jooq.generated.enums.PermissionType.workspace_admin;
}

return io.airbyte.db.instance.configs.jooq.generated.enums.PermissionType.valueOf(permissionType.value());
}

/**
* List all workspaces readable by user id, returning result ordered by workspace name. Supports
* keyword search.
* List all active workspaces readable by user id, returning result ordered by workspace name.
* Supports keyword search.
*/
public List<StandardWorkspace> listWorkspacesByUserId(UUID userId, Optional<String> keyword)
public List<StandardWorkspace> listActiveWorkspacesByUserId(UUID userId, Optional<String> keyword)
throws IOException {
final String searchKeyword = getSearchKeyword(keyword);
return database
.query(ctx -> ctx.fetch(listWorkspacesByUserIdAndPermissionTypeBasicQuery, userId,
getGrantingPermissionTypeArray(PermissionType.WORKSPACE_READER), searchKeyword))
.query(ctx -> ctx.fetch(
PermissionPersistenceHelper.LIST_ACTIVE_WORKSPACES_BY_USER_ID_AND_PERMISSION_TYPES_QUERY,
userId,
PermissionPersistenceHelper.getGrantingPermissionTypeArray(PermissionType.WORKSPACE_READER),
searchKeyword))
.stream()
.map(DbConverter::buildStandardWorkspace)
.toList();
Expand All @@ -189,13 +144,18 @@ public List<StandardWorkspace> listWorkspacesByUserId(UUID userId, Optional<Stri
public List<StandardWorkspace> listWorkspacesByUserIdPaginated(final ResourcesByUserQueryPaginated query, Optional<String> keyword)
throws IOException {
final String searchKeyword = getSearchKeyword(keyword);
final String workspaceQuery = listWorkspacesByUserIdAndPermissionTypeBasicQuery
final String workspaceQuery = PermissionPersistenceHelper.LIST_ACTIVE_WORKSPACES_BY_USER_ID_AND_PERMISSION_TYPES_QUERY
+ " LIMIT {3}"
+ " OFFSET {4}";

return database
.query(ctx -> ctx.fetch(workspaceQuery, query.userId(), getGrantingPermissionTypeArray(PermissionType.WORKSPACE_READER), searchKeyword,
query.pageSize(), query.rowOffset()))
.query(ctx -> ctx.fetch(
workspaceQuery,
query.userId(),
PermissionPersistenceHelper.getGrantingPermissionTypeArray(PermissionType.WORKSPACE_READER),
searchKeyword,
query.pageSize(),
query.rowOffset()))
.stream()
.map(DbConverter::buildStandardWorkspace)
.toList();
Expand Down
Loading

0 comments on commit 62cb0c0

Please sign in to comment.