Skip to content

Commit

Permalink
SSO: First user signed up in an Org gets OrganizationAdmin, the rest …
Browse files Browse the repository at this point in the history
…get OrganizationMember (#9372)
  • Loading branch information
pmossman committed Oct 19, 2023
1 parent 5bbb887 commit 6103a25
Show file tree
Hide file tree
Showing 9 changed files with 428 additions and 108 deletions.
11 changes: 11 additions & 0 deletions airbyte-api/src/main/openapi/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3581,10 +3581,13 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/PermissionRead"
"403":
$ref: "#/components/responses/ForbiddenResponse"
"404":
$ref: "#/components/responses/NotFoundResponse"
"422":
$ref: "#/components/responses/InvalidInputResponse"

/v1/permissions/delete:
post:
tags:
Expand All @@ -3600,6 +3603,8 @@ paths:
responses:
"204":
description: The resource was deleted successfully.
"403":
$ref: "#/components/responses/ForbiddenResponse"
"404":
$ref: "#/components/responses/NotFoundResponse"
"422":
Expand Down Expand Up @@ -8699,6 +8704,12 @@ components:
application/json:
schema:
$ref: "#/components/schemas/InvalidInputExceptionInfo"
ForbiddenResponse:
description: Operation forbidden
content:
application/json:
schema:
$ref: "#/components/schemas/KnownExceptionInfo"
ExceptionResponse:
description: Exception occurred; see message for details.
content:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2023 Airbyte, Inc., all rights reserved.
*/

package io.airbyte.commons.server.errors;

/**
* Exception when an operation is correctly formatted and syntactically valid, but not allowed due
* to the current state of the system. For example, deletion of a resource that should not be
* deleted according to business logic.
*/
public class OperationNotAllowedException extends KnownException {

public OperationNotAllowedException(final String message) {
super(message);
}

@Override
public int getHttpCode() {
return 403;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
import io.airbyte.api.model.generated.PermissionsCheckMultipleWorkspacesRequest;
import io.airbyte.commons.enums.Enums;
import io.airbyte.commons.lang.Exceptions;
import io.airbyte.commons.server.errors.OperationNotAllowedException;
import io.airbyte.config.ConfigSchema;
import io.airbyte.config.Permission;
import io.airbyte.config.helpers.PermissionHelper;
import io.airbyte.config.persistence.ConfigNotFoundException;
import io.airbyte.config.persistence.PermissionPersistence;
import io.airbyte.config.persistence.SQLOperationNotAllowedException;
import io.airbyte.data.services.WorkspaceService;
import io.airbyte.validation.json.JsonValidationException;
import jakarta.inject.Singleton;
Expand All @@ -31,6 +33,7 @@
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.jooq.exception.DataAccessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -164,19 +167,25 @@ public PermissionRead getPermission(final PermissionIdRequestBody permissionIdRe
* @return The updated permission.
* @throws IOException if unable to update the permissions.
* @throws ConfigNotFoundException if unable to update the permissions.
* @throws JsonValidationException if unable to update the permissions.
* @throws OperationNotAllowedException if update is prevented by business logic.
*/
public PermissionRead updatePermission(final PermissionUpdate permissionUpdate)
throws IOException, ConfigNotFoundException {
final Permission permission = new Permission()
throws IOException, ConfigNotFoundException, OperationNotAllowedException {
final Permission updatedPermission = new Permission()
.withPermissionId(permissionUpdate.getPermissionId())
.withPermissionType(Enums.convertTo(permissionUpdate.getPermissionType(), Permission.PermissionType.class))
.withUserId(permissionUpdate.getUserId())
.withWorkspaceId(permissionUpdate.getWorkspaceId())
.withOrganizationId(permissionUpdate.getOrganizationId());

permissionPersistence.writePermission(permission);

try {
permissionPersistence.writePermission(updatedPermission);
} catch (final DataAccessException e) {
if (e.getCause() instanceof SQLOperationNotAllowedException) {
throw new OperationNotAllowedException(e.getCause().getMessage());
} else {
throw new IOException(e);
}
}
return buildPermissionRead(permissionUpdate.getPermissionId());
}

Expand Down Expand Up @@ -345,10 +354,18 @@ public PermissionReadList listPermissionsByUser(final UUID userId) throws IOExce
*
* @param permissionIdRequestBody The permission to be deleted.
* @throws IOException if unable to delete the permission.
* @throws ConfigNotFoundException if unable to delete the permission.
* @throws OperationNotAllowedException if deletion is prevented by business logic.
*/
public void deletePermission(final PermissionIdRequestBody permissionIdRequestBody) throws IOException {
permissionPersistence.deletePermissionById(permissionIdRequestBody.getPermissionId());
try {
permissionPersistence.deletePermissionById(permissionIdRequestBody.getPermissionId());
} catch (final DataAccessException e) {
if (e.getCause() instanceof SQLOperationNotAllowedException) {
throw new OperationNotAllowedException(e.getCause().getMessage());
} else {
throw new IOException(e);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -329,24 +329,40 @@ public UserGetOrCreateByAuthIdResponse getOrCreateUserByAuthId(final UserAuthIdR
createInstanceAdminPermissionIfInitialUser(createdUser);

// If incoming SSO Config matches with existing org, find that org and add user to it;
final String ssoRealm = jwtUserResolver.get().resolveSsoRealm();
addUserToOrganizationIfSso(createdUser.getUserId());

return new UserGetOrCreateByAuthIdResponse().userRead(createdUser).newUserCreated(true);
}

private void addUserToOrganizationIfSso(final UUID userId) throws IOException {
final String ssoRealm = jwtUserResolver.orElseThrow().resolveSsoRealm();
if (ssoRealm != null) {
final Optional<Organization> attachedOrganization = organizationPersistence.getOrganizationBySsoConfigRealm(ssoRealm);
if (attachedOrganization.isPresent()) {
permissionHandler.createPermission(new io.airbyte.api.model.generated.PermissionCreate()
.workspaceId(null)
.organizationId(attachedOrganization.get().getOrganizationId())
.userId(createdUser.getUserId())
.permissionType(PermissionType.ORGANIZATION_ADMIN));
return new UserGetOrCreateByAuthIdResponse()
.userRead(createdUser)
.newUserCreated(true);
createPermissionForUserAndOrg(userId, attachedOrganization.get().getOrganizationId());
} else {
LOGGER.warn("New user with ID {} has an SSO realm {} but no Organization was found for it. No Organization permissions will be added.",
userId, ssoRealm);
}
}
// Otherwise, this indicates user is not associated with org (non-sso user signs up).
return new UserGetOrCreateByAuthIdResponse()
.userRead(createdUser)
.newUserCreated(true);
}

private void createPermissionForUserAndOrg(final UUID userId, final UUID orgId) throws IOException {
if (permissionPersistence.listPermissionsForOrganization(orgId).isEmpty()) {
LOGGER.debug("Organization {} does not have any users. Adding user {} with permission type {}",
orgId, userId, PermissionType.ORGANIZATION_ADMIN);
permissionHandler.createPermission(new io.airbyte.api.model.generated.PermissionCreate()
.organizationId(orgId)
.userId(userId)
.permissionType(PermissionType.ORGANIZATION_ADMIN));
} else {
LOGGER.debug("Organization {} already has existing users. Adding user {} with permission type {}",
orgId, userId, PermissionType.ORGANIZATION_MEMBER);
permissionHandler.createPermission(new io.airbyte.api.model.generated.PermissionCreate()
.organizationId(orgId)
.userId(userId)
.permissionType(PermissionType.ORGANIZATION_MEMBER));
}
}

private void createInstanceAdminPermissionIfInitialUser(final UserRead createdUser) throws IOException {
Expand Down
Loading

0 comments on commit 6103a25

Please sign in to comment.