diff --git a/server/src/main/java/io/unitycatalog/server/UnityCatalogServer.java b/server/src/main/java/io/unitycatalog/server/UnityCatalogServer.java index 8c633db96..f86c1a87a 100644 --- a/server/src/main/java/io/unitycatalog/server/UnityCatalogServer.java +++ b/server/src/main/java/io/unitycatalog/server/UnityCatalogServer.java @@ -138,11 +138,11 @@ private void addServices(ServerBuilder sb) { ModelService modelService = new ModelService(authorizer); // TODO: combine these into a single service in a follow-up PR TemporaryTableCredentialsService temporaryTableCredentialsService = - new TemporaryTableCredentialsService(credentialOperations); + new TemporaryTableCredentialsService(authorizer, credentialOperations); TemporaryVolumeCredentialsService temporaryVolumeCredentialsService = - new TemporaryVolumeCredentialsService(credentialOperations); + new TemporaryVolumeCredentialsService(authorizer, credentialOperations); TemporaryModelVersionCredentialsService temporaryModelVersionCredentialsService = - new TemporaryModelVersionCredentialsService(credentialOperations); + new TemporaryModelVersionCredentialsService(authorizer, credentialOperations); TemporaryPathCredentialsService temporaryPathCredentialsService = new TemporaryPathCredentialsService(credentialOperations); sb.service("/", (ctx, req) -> HttpResponse.of("Hello, Unity Catalog!")) diff --git a/server/src/main/java/io/unitycatalog/server/auth/decorator/KeyMapperUtil.java b/server/src/main/java/io/unitycatalog/server/auth/decorator/KeyMapperUtil.java new file mode 100644 index 000000000..0f16fd23b --- /dev/null +++ b/server/src/main/java/io/unitycatalog/server/auth/decorator/KeyMapperUtil.java @@ -0,0 +1,181 @@ +package io.unitycatalog.server.auth.decorator; + +import static io.unitycatalog.server.model.SecurableType.CATALOG; +import static io.unitycatalog.server.model.SecurableType.FUNCTION; +import static io.unitycatalog.server.model.SecurableType.METASTORE; +import static io.unitycatalog.server.model.SecurableType.REGISTERED_MODEL; +import static io.unitycatalog.server.model.SecurableType.SCHEMA; +import static io.unitycatalog.server.model.SecurableType.TABLE; +import static io.unitycatalog.server.model.SecurableType.VOLUME; + +import io.unitycatalog.server.model.CatalogInfo; +import io.unitycatalog.server.model.FunctionInfo; +import io.unitycatalog.server.model.RegisteredModelInfo; +import io.unitycatalog.server.model.SchemaInfo; +import io.unitycatalog.server.model.SecurableType; +import io.unitycatalog.server.model.TableInfo; +import io.unitycatalog.server.model.VolumeInfo; +import io.unitycatalog.server.persist.CatalogRepository; +import io.unitycatalog.server.persist.FunctionRepository; +import io.unitycatalog.server.persist.MetastoreRepository; +import io.unitycatalog.server.persist.ModelRepository; +import io.unitycatalog.server.persist.SchemaRepository; +import io.unitycatalog.server.persist.TableRepository; +import io.unitycatalog.server.persist.VolumeRepository; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class KeyMapperUtil { + public static Map mapResourceKeys( + Map resourceKeys) { + Map resourceIds = new HashMap<>(); + + if (resourceKeys.containsKey(CATALOG) + && resourceKeys.containsKey(SCHEMA) + && resourceKeys.containsKey(TABLE)) { + String fullName = + resourceKeys.get(CATALOG) + + "." + + resourceKeys.get(SCHEMA) + + "." + + resourceKeys.get(TABLE); + TableInfo table = TableRepository.getInstance().getTable(fullName); + resourceIds.put(TABLE, UUID.fromString(table.getTableId())); + } + + // If only TABLE is specified, assuming its value is a full table name (including catalog and + // schema) + if (!resourceKeys.containsKey(CATALOG) + && !resourceKeys.containsKey(SCHEMA) + && resourceKeys.containsKey(TABLE)) { + String fullName = (String) resourceKeys.get(TABLE); + // If the full name contains a dot, we assume it's a full name, otherwise we assume it's an id + TableInfo table = + fullName.contains(".") + ? TableRepository.getInstance().getTable(fullName) + : TableRepository.getInstance().getTableById(fullName); + String fullSchemaName = table.getCatalogName() + "." + table.getSchemaName(); + SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullSchemaName); + CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(table.getCatalogName()); + resourceIds.put(TABLE, UUID.fromString(table.getTableId())); + resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); + resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); + } + + if (resourceKeys.containsKey(CATALOG) + && resourceKeys.containsKey(SCHEMA) + && resourceKeys.containsKey(VOLUME)) { + String fullName = + resourceKeys.get(CATALOG) + + "." + + resourceKeys.get(SCHEMA) + + "." + + resourceKeys.get(VOLUME); + VolumeInfo volume = VolumeRepository.getInstance().getVolume(fullName); + resourceIds.put(VOLUME, UUID.fromString(volume.getVolumeId())); + } + + // If only VOLUME is specified, assuming its value is a full volume name (including catalog and + // schema) + if (!resourceKeys.containsKey(CATALOG) + && !resourceKeys.containsKey(SCHEMA) + && resourceKeys.containsKey(VOLUME)) { + String fullName = (String) resourceKeys.get(VOLUME); + // If the full name contains a dot, we assume it's a full name, otherwise we assume it's an id + VolumeInfo volume = + (fullName.contains(".")) + ? VolumeRepository.getInstance().getVolume(fullName) + : VolumeRepository.getInstance().getVolumeById(fullName); + String fullSchemaName = volume.getCatalogName() + "." + volume.getSchemaName(); + SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullSchemaName); + CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(volume.getCatalogName()); + resourceIds.put(VOLUME, UUID.fromString(volume.getVolumeId())); + resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); + resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); + } + + if (resourceKeys.containsKey(CATALOG) + && resourceKeys.containsKey(SCHEMA) + && resourceKeys.containsKey(FUNCTION)) { + String fullName = + resourceKeys.get(CATALOG) + + "." + + resourceKeys.get(SCHEMA) + + "." + + resourceKeys.get(FUNCTION); + FunctionInfo function = FunctionRepository.getInstance().getFunction(fullName); + resourceIds.put(FUNCTION, UUID.fromString(function.getFunctionId())); + } + + // If only FUNCTION is specified, assuming its value is a full volume name (including catalog + // and schema) + if (!resourceKeys.containsKey(CATALOG) + && !resourceKeys.containsKey(SCHEMA) + && resourceKeys.containsKey(FUNCTION)) { + String fullName = (String) resourceKeys.get(FUNCTION); + FunctionInfo function = FunctionRepository.getInstance().getFunction(fullName); + String fullSchemaName = function.getCatalogName() + "." + function.getSchemaName(); + SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullSchemaName); + CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(function.getCatalogName()); + resourceIds.put(FUNCTION, UUID.fromString(function.getFunctionId())); + resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); + resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); + } + + if (resourceKeys.containsKey(CATALOG) + && resourceKeys.containsKey(SCHEMA) + && resourceKeys.containsKey(REGISTERED_MODEL)) { + String fullName = + resourceKeys.get(CATALOG) + + "." + + resourceKeys.get(SCHEMA) + + "." + + resourceKeys.get(REGISTERED_MODEL); + RegisteredModelInfo model = ModelRepository.getInstance().getRegisteredModel(fullName); + resourceIds.put(REGISTERED_MODEL, UUID.fromString(model.getId())); + } + + // If only REGISTERED_MODEL is specified, assuming its value is a full volume name (including + // catalog and schema) + if (!resourceKeys.containsKey(CATALOG) + && !resourceKeys.containsKey(SCHEMA) + && resourceKeys.containsKey(REGISTERED_MODEL)) { + String fullName = (String) resourceKeys.get(REGISTERED_MODEL); + RegisteredModelInfo model = ModelRepository.getInstance().getRegisteredModel(fullName); + String fullSchemaName = model.getCatalogName() + "." + model.getSchemaName(); + SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullSchemaName); + CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(model.getCatalogName()); + resourceIds.put(REGISTERED_MODEL, UUID.fromString(model.getId())); + resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); + resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); + } + + if (resourceKeys.containsKey(CATALOG) && resourceKeys.containsKey(SCHEMA)) { + String fullName = resourceKeys.get(CATALOG) + "." + resourceKeys.get(SCHEMA); + SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullName); + resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); + } + + // if only SCHEMA is specified, assuming its value is a full schema name (including catalog) + if (!resourceKeys.containsKey(CATALOG) && resourceKeys.containsKey(SCHEMA)) { + String fullName = (String) resourceKeys.get(SCHEMA); + SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullName); + CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(schema.getCatalogName()); + resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); + resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); + } + + if (resourceKeys.containsKey(CATALOG)) { + String fullName = (String) resourceKeys.get(CATALOG); + CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(fullName); + resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); + } + + if (resourceKeys.containsKey(METASTORE)) { + resourceIds.put(METASTORE, MetastoreRepository.getInstance().getMetastoreId()); + } + + return resourceIds; + } +} diff --git a/server/src/main/java/io/unitycatalog/server/auth/decorator/UnityAccessDecorator.java b/server/src/main/java/io/unitycatalog/server/auth/decorator/UnityAccessDecorator.java index 5d2e8755b..e8ec65384 100644 --- a/server/src/main/java/io/unitycatalog/server/auth/decorator/UnityAccessDecorator.java +++ b/server/src/main/java/io/unitycatalog/server/auth/decorator/UnityAccessDecorator.java @@ -18,20 +18,7 @@ import io.unitycatalog.server.auth.annotation.AuthorizeKeys; import io.unitycatalog.server.exception.BaseException; import io.unitycatalog.server.exception.ErrorCode; -import io.unitycatalog.server.model.CatalogInfo; -import io.unitycatalog.server.model.FunctionInfo; -import io.unitycatalog.server.model.RegisteredModelInfo; -import io.unitycatalog.server.model.SchemaInfo; import io.unitycatalog.server.model.SecurableType; -import io.unitycatalog.server.model.TableInfo; -import io.unitycatalog.server.model.VolumeInfo; -import io.unitycatalog.server.persist.CatalogRepository; -import io.unitycatalog.server.persist.FunctionRepository; -import io.unitycatalog.server.persist.MetastoreRepository; -import io.unitycatalog.server.persist.ModelRepository; -import io.unitycatalog.server.persist.SchemaRepository; -import io.unitycatalog.server.persist.TableRepository; -import io.unitycatalog.server.persist.VolumeRepository; import io.unitycatalog.server.utils.IdentityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,13 +37,6 @@ import static io.unitycatalog.server.auth.decorator.KeyLocator.Source.PARAM; import static io.unitycatalog.server.auth.decorator.KeyLocator.Source.PAYLOAD; import static io.unitycatalog.server.auth.decorator.KeyLocator.Source.SYSTEM; -import static io.unitycatalog.server.model.SecurableType.CATALOG; -import static io.unitycatalog.server.model.SecurableType.FUNCTION; -import static io.unitycatalog.server.model.SecurableType.METASTORE; -import static io.unitycatalog.server.model.SecurableType.REGISTERED_MODEL; -import static io.unitycatalog.server.model.SecurableType.SCHEMA; -import static io.unitycatalog.server.model.SecurableType.TABLE; -import static io.unitycatalog.server.model.SecurableType.VOLUME; /** * Armeria access control Decorator. @@ -192,7 +172,7 @@ private static Object findPayloadValue(String key, Map payload) private void checkAuthorization(UUID principal, String expression, Map resourceKeys) { LOGGER.debug("resourceKeys = {}", resourceKeys); - Map resourceIds = mapResourceKeys(resourceKeys); + Map resourceIds = KeyMapperUtil.mapResourceKeys(resourceKeys); if (!resourceIds.keySet().containsAll(resourceKeys.keySet())) { LOGGER.warn("Some resource keys have unresolved ids."); @@ -205,110 +185,6 @@ private void checkAuthorization(UUID principal, String expression, Map mapResourceKeys(Map resourceKeys) { - Map resourceIds = new HashMap<>(); - - if (resourceKeys.containsKey(CATALOG) && resourceKeys.containsKey(SCHEMA) && resourceKeys.containsKey(TABLE)) { - String fullName = resourceKeys.get(CATALOG) + "." + resourceKeys.get(SCHEMA) + "." + resourceKeys.get(TABLE); - TableInfo table = TableRepository.getInstance().getTable(fullName); - resourceIds.put(TABLE, UUID.fromString(table.getTableId())); - } - - // If only TABLE is specified, assuming its value is a full table name (including catalog and schema) - if (!resourceKeys.containsKey(CATALOG) && !resourceKeys.containsKey(SCHEMA) && resourceKeys.containsKey(TABLE)) { - String fullName = (String) resourceKeys.get(TABLE); - TableInfo table = TableRepository.getInstance().getTable(fullName); - String fullSchemaName = table.getCatalogName() + "." + table.getSchemaName(); - SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullSchemaName); - CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(table.getCatalogName()); - resourceIds.put(TABLE, UUID.fromString(table.getTableId())); - resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); - resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); - } - - if (resourceKeys.containsKey(CATALOG) && resourceKeys.containsKey(SCHEMA) && resourceKeys.containsKey(VOLUME)) { - String fullName = resourceKeys.get(CATALOG) + "." + resourceKeys.get(SCHEMA) + "." + resourceKeys.get(VOLUME); - VolumeInfo volume = VolumeRepository.getInstance().getVolume(fullName); - resourceIds.put(VOLUME, UUID.fromString(volume.getVolumeId())); - } - - // If only VOLUME is specified, assuming its value is a full volume name (including catalog and schema) - if (!resourceKeys.containsKey(CATALOG) && !resourceKeys.containsKey(SCHEMA) && resourceKeys.containsKey(VOLUME)) { - String fullName = (String) resourceKeys.get(VOLUME); - VolumeInfo volume = VolumeRepository.getInstance().getVolume(fullName); - String fullSchemaName = volume.getCatalogName() + "." + volume.getSchemaName(); - SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullSchemaName); - CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(volume.getCatalogName()); - resourceIds.put(VOLUME, UUID.fromString(volume.getVolumeId())); - resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); - resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); - } - - if (resourceKeys.containsKey(CATALOG) && resourceKeys.containsKey(SCHEMA) && resourceKeys.containsKey(FUNCTION)) { - String fullName = resourceKeys.get(CATALOG) + "." + resourceKeys.get(SCHEMA) + "." + resourceKeys.get(FUNCTION); - FunctionInfo function = FunctionRepository.getInstance().getFunction(fullName); - resourceIds.put(FUNCTION, UUID.fromString(function.getFunctionId())); - } - - // If only FUNCTION is specified, assuming its value is a full volume name (including catalog and schema) - if (!resourceKeys.containsKey(CATALOG) && !resourceKeys.containsKey(SCHEMA) && resourceKeys.containsKey(FUNCTION)) { - String fullName = (String) resourceKeys.get(FUNCTION); - FunctionInfo function = FunctionRepository.getInstance().getFunction(fullName); - String fullSchemaName = function.getCatalogName() + "." + function.getSchemaName(); - SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullSchemaName); - CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(function.getCatalogName()); - resourceIds.put(FUNCTION, UUID.fromString(function.getFunctionId())); - resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); - resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); - } - - if (resourceKeys.containsKey(CATALOG) && resourceKeys.containsKey(SCHEMA) && resourceKeys.containsKey(REGISTERED_MODEL)) { - String fullName = resourceKeys.get(CATALOG) + "." + resourceKeys.get(SCHEMA) + "." + resourceKeys.get(REGISTERED_MODEL); - RegisteredModelInfo model = ModelRepository.getInstance().getRegisteredModel(fullName); - resourceIds.put(REGISTERED_MODEL, UUID.fromString(model.getId())); - } - - // If only REGISTERED_MODEL is specified, assuming its value is a full volume name (including catalog and schema) - if (!resourceKeys.containsKey(CATALOG) && !resourceKeys.containsKey(SCHEMA) && resourceKeys.containsKey(REGISTERED_MODEL)) { - String fullName = (String) resourceKeys.get(REGISTERED_MODEL); - RegisteredModelInfo model = ModelRepository.getInstance().getRegisteredModel(fullName); - String fullSchemaName = model.getCatalogName() + "." + model.getSchemaName(); - SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullSchemaName); - CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(model.getCatalogName()); - resourceIds.put(REGISTERED_MODEL, UUID.fromString(model.getId())); - resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); - resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); - } - - - if (resourceKeys.containsKey(CATALOG) && resourceKeys.containsKey(SCHEMA)) { - String fullName = resourceKeys.get(CATALOG) + "." + resourceKeys.get(SCHEMA); - SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullName); - resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); - } - - // if only SCHEMA is specified, assuming its value is a full schema name (including catalog) - if (!resourceKeys.containsKey(CATALOG) && resourceKeys.containsKey(SCHEMA)) { - String fullName = (String) resourceKeys.get(SCHEMA); - SchemaInfo schema = SchemaRepository.getInstance().getSchema(fullName); - CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(schema.getCatalogName()); - resourceIds.put(SCHEMA, UUID.fromString(schema.getSchemaId())); - resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); - } - - if (resourceKeys.containsKey(CATALOG)) { - String fullName = (String) resourceKeys.get(CATALOG); - CatalogInfo catalog = CatalogRepository.getInstance().getCatalog(fullName); - resourceIds.put(CATALOG, UUID.fromString(catalog.getId())); - } - - if (resourceKeys.containsKey(METASTORE)) { - resourceIds.put(METASTORE, MetastoreRepository.getInstance().getMetastoreId()); - } - - return resourceIds; - } - private static String findAuthorizeExpression(Method method) { // TODO: Cache this by method diff --git a/server/src/main/java/io/unitycatalog/server/persist/TableRepository.java b/server/src/main/java/io/unitycatalog/server/persist/TableRepository.java index e65bdef89..96346799e 100644 --- a/server/src/main/java/io/unitycatalog/server/persist/TableRepository.java +++ b/server/src/main/java/io/unitycatalog/server/persist/TableRepository.java @@ -3,6 +3,7 @@ import io.unitycatalog.server.exception.BaseException; import io.unitycatalog.server.exception.ErrorCode; import io.unitycatalog.server.model.*; +import io.unitycatalog.server.persist.dao.CatalogInfoDAO; import io.unitycatalog.server.persist.dao.PropertyDAO; import io.unitycatalog.server.persist.dao.SchemaInfoDAO; import io.unitycatalog.server.persist.dao.TableInfoDAO; @@ -46,7 +47,20 @@ public TableInfo getTableById(String tableId) { if (tableInfoDAO == null) { throw new BaseException(ErrorCode.NOT_FOUND, "Table not found: " + tableId); } + SchemaInfoDAO schemaInfoDAO = session.get(SchemaInfoDAO.class, tableInfoDAO.getSchemaId()); + if (schemaInfoDAO == null) { + throw new BaseException( + ErrorCode.NOT_FOUND, "Schema not found: " + tableInfoDAO.getSchemaId()); + } + CatalogInfoDAO catalogInfoDAO = + session.get(CatalogInfoDAO.class, schemaInfoDAO.getCatalogId()); + if (catalogInfoDAO == null) { + throw new BaseException( + ErrorCode.NOT_FOUND, "Catalog not found: " + schemaInfoDAO.getCatalogId()); + } TableInfo tableInfo = tableInfoDAO.toTableInfo(true); + tableInfo.setSchemaName(schemaInfoDAO.getName()); + tableInfo.setCatalogName(catalogInfoDAO.getName()); tx.commit(); return tableInfo; } catch (Exception e) { diff --git a/server/src/main/java/io/unitycatalog/server/persist/VolumeRepository.java b/server/src/main/java/io/unitycatalog/server/persist/VolumeRepository.java index 62df1c65f..232bf5f39 100644 --- a/server/src/main/java/io/unitycatalog/server/persist/VolumeRepository.java +++ b/server/src/main/java/io/unitycatalog/server/persist/VolumeRepository.java @@ -3,6 +3,7 @@ import io.unitycatalog.server.exception.BaseException; import io.unitycatalog.server.exception.ErrorCode; import io.unitycatalog.server.model.*; +import io.unitycatalog.server.persist.dao.CatalogInfoDAO; import io.unitycatalog.server.persist.dao.SchemaInfoDAO; import io.unitycatalog.server.persist.dao.VolumeInfoDAO; import io.unitycatalog.server.persist.utils.FileUtils; @@ -146,13 +147,26 @@ public VolumeInfo getVolumeById(String volumeId) { session.setDefaultReadOnly(true); Transaction tx = session.beginTransaction(); try { - Query query = - session.createQuery("FROM VolumeInfoDAO WHERE id = :value", VolumeInfoDAO.class); - query.setParameter("value", UUID.fromString(volumeId)); - query.setMaxResults(1); - VolumeInfoDAO volumeInfoDAO = query.uniqueResult(); + VolumeInfoDAO volumeInfoDAO = session.get(VolumeInfoDAO.class, UUID.fromString(volumeId)); + if (volumeInfoDAO == null) { + throw new BaseException(ErrorCode.NOT_FOUND, "Table not found: " + volumeId); + } + SchemaInfoDAO schemaInfoDAO = session.get(SchemaInfoDAO.class, volumeInfoDAO.getSchemaId()); + if (schemaInfoDAO == null) { + throw new BaseException( + ErrorCode.NOT_FOUND, "Schema not found: " + volumeInfoDAO.getSchemaId()); + } + CatalogInfoDAO catalogInfoDAO = + session.get(CatalogInfoDAO.class, schemaInfoDAO.getCatalogId()); + if (catalogInfoDAO == null) { + throw new BaseException( + ErrorCode.NOT_FOUND, "Catalog not found: " + schemaInfoDAO.getCatalogId()); + } tx.commit(); - return volumeInfoDAO.toVolumeInfo(); + VolumeInfo volumeInfo = volumeInfoDAO.toVolumeInfo(); + volumeInfo.setSchemaName(schemaInfoDAO.getName()); + volumeInfo.setCatalogName(catalogInfoDAO.getName()); + return volumeInfo; } catch (Exception e) { tx.rollback(); throw e; diff --git a/server/src/main/java/io/unitycatalog/server/service/TemporaryModelVersionCredentialsService.java b/server/src/main/java/io/unitycatalog/server/service/TemporaryModelVersionCredentialsService.java index 19fb2d43d..2fa510b74 100644 --- a/server/src/main/java/io/unitycatalog/server/service/TemporaryModelVersionCredentialsService.java +++ b/server/src/main/java/io/unitycatalog/server/service/TemporaryModelVersionCredentialsService.java @@ -3,6 +3,9 @@ import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.server.annotation.ExceptionHandler; import com.linecorp.armeria.server.annotation.Post; +import io.unitycatalog.server.auth.UnityCatalogAuthorizer; +import io.unitycatalog.server.auth.decorator.KeyMapperUtil; +import io.unitycatalog.server.auth.decorator.UnityAccessEvaluator; import io.unitycatalog.server.exception.BaseException; import io.unitycatalog.server.exception.ErrorCode; import io.unitycatalog.server.exception.GlobalExceptionHandler; @@ -11,9 +14,16 @@ import io.unitycatalog.server.persist.utils.RepositoryUtils; import io.unitycatalog.server.service.credential.CredentialOperations; import io.unitycatalog.server.service.credential.CredentialContext; +import io.unitycatalog.server.utils.IdentityUtils; +import lombok.SneakyThrows; +import java.util.Map; import java.util.Set; +import static io.unitycatalog.server.model.SecurableType.CATALOG; +import static io.unitycatalog.server.model.SecurableType.METASTORE; +import static io.unitycatalog.server.model.SecurableType.REGISTERED_MODEL; +import static io.unitycatalog.server.model.SecurableType.SCHEMA; import static io.unitycatalog.server.service.credential.CredentialContext.Privilege.SELECT; import static io.unitycatalog.server.service.credential.CredentialContext.Privilege.UPDATE; @@ -22,15 +32,18 @@ public class TemporaryModelVersionCredentialsService { private static final ModelRepository MODEL_REPOSITORY = ModelRepository.getInstance(); + private final UnityAccessEvaluator evaluator; private final CredentialOperations credentialOps; - public TemporaryModelVersionCredentialsService(CredentialOperations credentialOps) { + @SneakyThrows + public TemporaryModelVersionCredentialsService(UnityCatalogAuthorizer authorizer, CredentialOperations credentialOps) { + this.evaluator = new UnityAccessEvaluator(authorizer); this.credentialOps = credentialOps; } @Post("") - public HttpResponse generateTemporaryModelVersionCredentials( - GenerateTemporaryModelVersionCredential generateTemporaryModelVersionCredentials) { + public HttpResponse generateTemporaryModelVersionCredentials(GenerateTemporaryModelVersionCredential generateTemporaryModelVersionCredentials) { + authorizeForOperation(generateTemporaryModelVersionCredentials); long modelVersion = generateTemporaryModelVersionCredentials.getVersion(); String catalogName = generateTemporaryModelVersionCredentials.getCatalogName(); @@ -66,4 +79,32 @@ private Set modelVersionOperationToPrivileges(Model throw new BaseException(ErrorCode.INVALID_ARGUMENT, "Unknown operation in the request: " + ModelVersionOperation.UNKNOWN_MODEL_VERSION_OPERATION); }; } + + private void authorizeForOperation(GenerateTemporaryModelVersionCredential generateTemporaryModelVersionCredentials) { + + // TODO: This is a short term solution to conditional expression evaluation based on additional request parameters. + // This should be replaced with more direct annotations and syntax in the future. + + String readExpression = """ + #authorizeAny(#principal, #registered_model, OWNER, EXECUTE) && #authorizeAny(#principal, #schema, OWNER, USE_SCHEMA) && #authorizeAny(#principal, #catalog, OWNER, USE_CATALOG) + """; + + String writeExpression = """ + (#authorize(#principal, #registered_model, OWNER) && #authorizeAny(#principal, #schema, OWNER, USE_SCHEMA) && #authorizeAny(#principal, #catalog, OWNER, USE_CATALOG)) + """; + + String authorizeExpression = + generateTemporaryModelVersionCredentials.getOperation() == ModelVersionOperation.READ_MODEL_VERSION ? + readExpression : writeExpression; + + Map resourceKeys = KeyMapperUtil.mapResourceKeys( + Map.of(METASTORE, "metastore", + CATALOG, generateTemporaryModelVersionCredentials.getCatalogName(), + SCHEMA, generateTemporaryModelVersionCredentials.getSchemaName(), + REGISTERED_MODEL, generateTemporaryModelVersionCredentials.getModelName())); + + if (!evaluator.evaluate(IdentityUtils.findPrincipalId(), authorizeExpression, resourceKeys)) { + throw new BaseException(ErrorCode.PERMISSION_DENIED, "Access denied."); + } + } } diff --git a/server/src/main/java/io/unitycatalog/server/service/TemporaryPathCredentialsService.java b/server/src/main/java/io/unitycatalog/server/service/TemporaryPathCredentialsService.java index 6614be80d..86f5c149d 100644 --- a/server/src/main/java/io/unitycatalog/server/service/TemporaryPathCredentialsService.java +++ b/server/src/main/java/io/unitycatalog/server/service/TemporaryPathCredentialsService.java @@ -3,6 +3,8 @@ import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.server.annotation.ExceptionHandler; import com.linecorp.armeria.server.annotation.Post; +import io.unitycatalog.server.auth.annotation.AuthorizeExpression; +import io.unitycatalog.server.auth.annotation.AuthorizeKey; import io.unitycatalog.server.exception.GlobalExceptionHandler; import io.unitycatalog.server.model.GenerateTemporaryPathCredential; import io.unitycatalog.server.model.PathOperation; @@ -12,6 +14,7 @@ import java.util.Collections; import java.util.Set; +import static io.unitycatalog.server.model.SecurableType.METASTORE; import static io.unitycatalog.server.service.credential.CredentialContext.Privilege.SELECT; import static io.unitycatalog.server.service.credential.CredentialContext.Privilege.UPDATE; @@ -24,6 +27,8 @@ public TemporaryPathCredentialsService(CredentialOperations credentialOps) { } @Post("") + @AuthorizeExpression("#authorize(#principal, #metastore, OWNER)") + @AuthorizeKey(METASTORE) public HttpResponse generateTemporaryPathCredential( GenerateTemporaryPathCredential generateTemporaryPathCredential) { return HttpResponse.ofJson( diff --git a/server/src/main/java/io/unitycatalog/server/service/TemporaryTableCredentialsService.java b/server/src/main/java/io/unitycatalog/server/service/TemporaryTableCredentialsService.java index 892738de6..6a11876dc 100644 --- a/server/src/main/java/io/unitycatalog/server/service/TemporaryTableCredentialsService.java +++ b/server/src/main/java/io/unitycatalog/server/service/TemporaryTableCredentialsService.java @@ -3,17 +3,35 @@ import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.server.annotation.ExceptionHandler; import com.linecorp.armeria.server.annotation.Post; +import io.unitycatalog.server.auth.UnityCatalogAuthorizer; +import io.unitycatalog.server.auth.annotation.AuthorizeExpression; +import io.unitycatalog.server.auth.annotation.AuthorizeKey; +import io.unitycatalog.server.auth.decorator.KeyMapperUtil; +import io.unitycatalog.server.auth.decorator.UnityAccessEvaluator; +import io.unitycatalog.server.exception.BaseException; +import io.unitycatalog.server.exception.ErrorCode; import io.unitycatalog.server.exception.GlobalExceptionHandler; +import io.unitycatalog.server.model.GenerateTemporaryModelVersionCredential; import io.unitycatalog.server.model.GenerateTemporaryTableCredential; +import io.unitycatalog.server.model.ModelVersionOperation; +import io.unitycatalog.server.model.SecurableType; import io.unitycatalog.server.model.TableInfo; import io.unitycatalog.server.model.TableOperation; import io.unitycatalog.server.persist.TableRepository; import io.unitycatalog.server.service.credential.CredentialContext; import io.unitycatalog.server.service.credential.CredentialOperations; +import io.unitycatalog.server.utils.IdentityUtils; +import lombok.SneakyThrows; import java.util.Collections; +import java.util.Map; import java.util.Set; +import static io.unitycatalog.server.model.SecurableType.CATALOG; +import static io.unitycatalog.server.model.SecurableType.METASTORE; +import static io.unitycatalog.server.model.SecurableType.REGISTERED_MODEL; +import static io.unitycatalog.server.model.SecurableType.SCHEMA; +import static io.unitycatalog.server.model.SecurableType.TABLE; import static io.unitycatalog.server.service.credential.CredentialContext.Privilege.SELECT; import static io.unitycatalog.server.service.credential.CredentialContext.Privilege.UPDATE; @@ -22,15 +40,19 @@ public class TemporaryTableCredentialsService { private static final TableRepository TABLE_REPOSITORY = TableRepository.getInstance(); + private final UnityAccessEvaluator evaluator; private final CredentialOperations credentialOps; - public TemporaryTableCredentialsService(CredentialOperations credentialOps) { + @SneakyThrows + public TemporaryTableCredentialsService(UnityCatalogAuthorizer authorizer, CredentialOperations credentialOps) { + this.evaluator = new UnityAccessEvaluator(authorizer); this.credentialOps = credentialOps; } @Post("") - public HttpResponse generateTemporaryTableCredential( - GenerateTemporaryTableCredential generateTemporaryTableCredential) { + public HttpResponse generateTemporaryTableCredential(GenerateTemporaryTableCredential generateTemporaryTableCredential) { + authorizeForOperation(generateTemporaryTableCredential); + String tableId = generateTemporaryTableCredential.getTableId(); TableInfo tableInfo = TABLE_REPOSITORY.getTableById(tableId); return HttpResponse.ofJson(credentialOps @@ -45,4 +67,32 @@ private Set tableOperationToPrivileges(TableOperati case UNKNOWN_TABLE_OPERATION -> Collections.emptySet(); }; } + + private void authorizeForOperation(GenerateTemporaryTableCredential generateTemporaryTableCredential) { + + // TODO: This is a short term solution to conditional expression evaluation based on additional request parameters. + // This should be replaced with more direct annotations and syntax in the future. + + String readExpression = """ + #authorizeAny(#principal, #schema, OWNER, USE_SCHEMA) && #authorizeAny(#principal, #catalog, OWNER, USE_CATALOG) && #authorizeAny(#principal, #table, OWNER, SELECT) + """; + + // TODO: add MODIFY to the expression + String writeExpression = """ + #authorizeAny(#principal, #schema, OWNER, USE_SCHEMA) && #authorizeAny(#principal, #catalog, OWNER, USE_CATALOG) && #authorize(#principal, #table, OWNER) + """; + + String authorizeExpression = + generateTemporaryTableCredential.getOperation() == TableOperation.READ ? + readExpression : writeExpression; + + Map resourceKeys = KeyMapperUtil.mapResourceKeys( + Map.of(METASTORE, "metastore", + TABLE, generateTemporaryTableCredential.getTableId())); + + if (!evaluator.evaluate(IdentityUtils.findPrincipalId(), authorizeExpression, resourceKeys)) { + throw new BaseException(ErrorCode.PERMISSION_DENIED, "Access denied."); + } + } + } diff --git a/server/src/main/java/io/unitycatalog/server/service/TemporaryVolumeCredentialsService.java b/server/src/main/java/io/unitycatalog/server/service/TemporaryVolumeCredentialsService.java index 7ee23c06d..ba4ebf842 100644 --- a/server/src/main/java/io/unitycatalog/server/service/TemporaryVolumeCredentialsService.java +++ b/server/src/main/java/io/unitycatalog/server/service/TemporaryVolumeCredentialsService.java @@ -3,19 +3,28 @@ import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.server.annotation.ExceptionHandler; import com.linecorp.armeria.server.annotation.Post; +import io.unitycatalog.server.auth.UnityCatalogAuthorizer; +import io.unitycatalog.server.auth.decorator.KeyMapperUtil; +import io.unitycatalog.server.auth.decorator.UnityAccessEvaluator; import io.unitycatalog.server.exception.BaseException; import io.unitycatalog.server.exception.ErrorCode; import io.unitycatalog.server.exception.GlobalExceptionHandler; import io.unitycatalog.server.model.GenerateTemporaryVolumeCredential; +import io.unitycatalog.server.model.SecurableType; import io.unitycatalog.server.model.VolumeInfo; import io.unitycatalog.server.model.VolumeOperation; import io.unitycatalog.server.persist.VolumeRepository; import io.unitycatalog.server.service.credential.CredentialContext; import io.unitycatalog.server.service.credential.CredentialOperations; +import io.unitycatalog.server.utils.IdentityUtils; +import lombok.SneakyThrows; import java.util.Collections; +import java.util.Map; import java.util.Set; +import static io.unitycatalog.server.model.SecurableType.METASTORE; +import static io.unitycatalog.server.model.SecurableType.VOLUME; import static io.unitycatalog.server.service.credential.CredentialContext.Privilege.SELECT; import static io.unitycatalog.server.service.credential.CredentialContext.Privilege.UPDATE; @@ -24,15 +33,19 @@ public class TemporaryVolumeCredentialsService { private static final VolumeRepository VOLUME_REPOSITORY = VolumeRepository.getInstance(); + private final UnityAccessEvaluator evaluator; private final CredentialOperations credentialOps; - public TemporaryVolumeCredentialsService(CredentialOperations credentialOps) { + @SneakyThrows + public TemporaryVolumeCredentialsService(UnityCatalogAuthorizer authorizer, CredentialOperations credentialOps) { + this.evaluator = new UnityAccessEvaluator(authorizer); this.credentialOps = credentialOps; } @Post("") - public HttpResponse generateTemporaryTableCredential( - GenerateTemporaryVolumeCredential generateTemporaryVolumeCredential) { + public HttpResponse generateTemporaryTableCredential(GenerateTemporaryVolumeCredential generateTemporaryVolumeCredential) { + authorizeForOperation(generateTemporaryVolumeCredential); + String volumeId = generateTemporaryVolumeCredential.getVolumeId(); if (volumeId.isEmpty()) { throw new BaseException(ErrorCode.INVALID_ARGUMENT, "Volume ID is required."); @@ -51,4 +64,34 @@ private Set volumeOperationToPrivileges(VolumeOpera case UNKNOWN_VOLUME_OPERATION -> Collections.emptySet(); }; } + + private void authorizeForOperation(GenerateTemporaryVolumeCredential generateTemporaryVolumeCredential) { + + // TODO: This is a short term solution to conditional expression evaluation based on additional request parameters. + // This should be replaced with more direct annotations and syntax in the future. + + String readExpression = """ + #authorizeAny(#principal, #schema, OWNER, USE_SCHEMA) && #authorizeAny(#principal, #catalog, OWNER, USE_CATALOG) && #authorizeAny(#principal, #volume, OWNER, READ_VOLUME) + """; + + // TODO: add WRITE_VOLUME to the expression + String writeExpression = """ + #authorizeAny(#principal, #catalog, OWNER, USE_CATALOG) && + #authorizeAny(#principal, #schema, OWNER, USE_SCHEMA) && + #authorize(#principal, #volume, OWNER) + """; + + String authorizeExpression = + generateTemporaryVolumeCredential.getOperation() == VolumeOperation.READ_VOLUME ? + readExpression : writeExpression; + + Map resourceKeys = KeyMapperUtil.mapResourceKeys( + Map.of(METASTORE, "metastore", + VOLUME, generateTemporaryVolumeCredential.getVolumeId())); + + if (!evaluator.evaluate(IdentityUtils.findPrincipalId(), authorizeExpression, resourceKeys)) { + throw new BaseException(ErrorCode.PERMISSION_DENIED, "Access denied."); + } + } + }