getPrivilegesOnSecondary() {
+ return privilegesOnSecondary;
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizer.java b/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizer.java
new file mode 100644
index 0000000000..29f40daaba
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizer.java
@@ -0,0 +1,630 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.auth;
+
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_CREATE;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_DROP;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_FULL_METADATA;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_LIST;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_LIST_GRANTS;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_MANAGE_ACCESS;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_MANAGE_CONTENT;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_MANAGE_GRANTS_ON_SECURABLE;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_MANAGE_METADATA;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_READ_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_CREATE;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_DROP;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_FULL_METADATA;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_LIST;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_LIST_GRANTS;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_READ_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_USAGE;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_WRITE_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.CATALOG_WRITE_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_CREATE;
+import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_DROP;
+import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_FULL_METADATA;
+import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_LIST;
+import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_LIST_GRANTS;
+import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_MANAGE_GRANTS_ON_SECURABLE;
+import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_READ_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_WRITE_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_CREATE;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_DROP;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_FULL_METADATA;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_LIST;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_LIST_GRANTS;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_READ_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_RESET_CREDENTIALS;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_CREATE;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_DROP;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_LIST;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_LIST_GRANTS;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_READ_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_USAGE;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_WRITE_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROTATE_CREDENTIALS;
+import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.SERVICE_MANAGE_ACCESS;
+import static io.polaris.core.entity.PolarisPrivilege.TABLE_CREATE;
+import static io.polaris.core.entity.PolarisPrivilege.TABLE_DROP;
+import static io.polaris.core.entity.PolarisPrivilege.TABLE_FULL_METADATA;
+import static io.polaris.core.entity.PolarisPrivilege.TABLE_LIST;
+import static io.polaris.core.entity.PolarisPrivilege.TABLE_LIST_GRANTS;
+import static io.polaris.core.entity.PolarisPrivilege.TABLE_MANAGE_GRANTS_ON_SECURABLE;
+import static io.polaris.core.entity.PolarisPrivilege.TABLE_READ_DATA;
+import static io.polaris.core.entity.PolarisPrivilege.TABLE_READ_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_DATA;
+import static io.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.VIEW_CREATE;
+import static io.polaris.core.entity.PolarisPrivilege.VIEW_DROP;
+import static io.polaris.core.entity.PolarisPrivilege.VIEW_FULL_METADATA;
+import static io.polaris.core.entity.PolarisPrivilege.VIEW_LIST;
+import static io.polaris.core.entity.PolarisPrivilege.VIEW_LIST_GRANTS;
+import static io.polaris.core.entity.PolarisPrivilege.VIEW_MANAGE_GRANTS_ON_SECURABLE;
+import static io.polaris.core.entity.PolarisPrivilege.VIEW_READ_PROPERTIES;
+import static io.polaris.core.entity.PolarisPrivilege.VIEW_WRITE_PROPERTIES;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.SetMultimap;
+import io.polaris.core.PolarisConfiguration;
+import io.polaris.core.PolarisConfigurationStore;
+import io.polaris.core.context.CallContext;
+import io.polaris.core.entity.PolarisEntityConstants;
+import io.polaris.core.entity.PolarisGrantRecord;
+import io.polaris.core.entity.PolarisPrivilege;
+import io.polaris.core.persistence.PolarisResolvedPathWrapper;
+import io.polaris.core.persistence.ResolvedPolarisEntity;
+import java.util.List;
+import java.util.Set;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Performs hierarchical resolution logic by matching the transively expanded set of grants to a
+ * calling principal against the cascading permissions over the parent hierarchy of a target
+ * Securable.
+ *
+ * Additionally, encompasses "specialty" permission resolution logic, such as checking whether
+ * the expanded roles of the calling Principal hold SERVICE_MANAGE_ACCESS on the "root" catalog,
+ * which translates into a cross-catalog permission.
+ */
+public class PolarisAuthorizer {
+ private static final Logger LOG = LoggerFactory.getLogger(PolarisAuthorizer.class);
+
+ private static final SetMultimap SUPER_PRIVILEGES =
+ HashMultimap.create();
+
+ static {
+ SUPER_PRIVILEGES.putAll(SERVICE_MANAGE_ACCESS, List.of(SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(CATALOG_MANAGE_ACCESS, List.of(CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(CATALOG_ROLE_USAGE, List.of(CATALOG_ROLE_USAGE));
+ SUPER_PRIVILEGES.putAll(PRINCIPAL_ROLE_USAGE, List.of(PRINCIPAL_ROLE_USAGE));
+
+ // Namespace, Table, View privileges
+ SUPER_PRIVILEGES.putAll(
+ NAMESPACE_CREATE,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ NAMESPACE_CREATE,
+ NAMESPACE_FULL_METADATA));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_CREATE,
+ List.of(
+ CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, TABLE_CREATE, TABLE_FULL_METADATA));
+ SUPER_PRIVILEGES.putAll(
+ VIEW_CREATE,
+ List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, VIEW_CREATE, VIEW_FULL_METADATA));
+ SUPER_PRIVILEGES.putAll(
+ NAMESPACE_DROP,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ NAMESPACE_DROP,
+ NAMESPACE_FULL_METADATA));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_DROP,
+ List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, TABLE_DROP, TABLE_FULL_METADATA));
+ SUPER_PRIVILEGES.putAll(
+ VIEW_DROP,
+ List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, VIEW_DROP, VIEW_FULL_METADATA));
+ SUPER_PRIVILEGES.putAll(
+ NAMESPACE_LIST,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ NAMESPACE_CREATE,
+ NAMESPACE_FULL_METADATA,
+ NAMESPACE_LIST,
+ NAMESPACE_READ_PROPERTIES,
+ NAMESPACE_WRITE_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_LIST,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_CREATE,
+ TABLE_FULL_METADATA,
+ TABLE_LIST,
+ TABLE_READ_DATA,
+ TABLE_READ_PROPERTIES,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ VIEW_LIST,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ VIEW_CREATE,
+ VIEW_FULL_METADATA,
+ VIEW_LIST,
+ VIEW_READ_PROPERTIES,
+ VIEW_WRITE_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ NAMESPACE_READ_PROPERTIES,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ NAMESPACE_FULL_METADATA,
+ NAMESPACE_READ_PROPERTIES,
+ NAMESPACE_WRITE_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_READ_PROPERTIES,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_READ_DATA,
+ TABLE_READ_PROPERTIES,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ VIEW_READ_PROPERTIES,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ VIEW_FULL_METADATA,
+ VIEW_READ_PROPERTIES,
+ VIEW_WRITE_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ NAMESPACE_WRITE_PROPERTIES,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ NAMESPACE_FULL_METADATA,
+ NAMESPACE_WRITE_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_WRITE_PROPERTIES,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ VIEW_WRITE_PROPERTIES,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ VIEW_FULL_METADATA,
+ VIEW_WRITE_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_READ_DATA, List.of(CATALOG_MANAGE_CONTENT, TABLE_READ_DATA, TABLE_WRITE_DATA));
+ SUPER_PRIVILEGES.putAll(TABLE_WRITE_DATA, List.of(CATALOG_MANAGE_CONTENT, TABLE_WRITE_DATA));
+ SUPER_PRIVILEGES.putAll(
+ NAMESPACE_FULL_METADATA,
+ List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, NAMESPACE_FULL_METADATA));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_FULL_METADATA,
+ List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, TABLE_FULL_METADATA));
+ SUPER_PRIVILEGES.putAll(
+ VIEW_FULL_METADATA,
+ List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, VIEW_FULL_METADATA));
+
+ // Catalog privileges
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_MANAGE_METADATA, List.of(CATALOG_MANAGE_METADATA, CATALOG_MANAGE_CONTENT));
+ SUPER_PRIVILEGES.putAll(CATALOG_MANAGE_CONTENT, List.of(CATALOG_MANAGE_CONTENT));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_CREATE, List.of(CATALOG_CREATE, CATALOG_FULL_METADATA, SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_DROP, List.of(CATALOG_DROP, CATALOG_FULL_METADATA, SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_LIST,
+ List.of(
+ CATALOG_CREATE,
+ CATALOG_FULL_METADATA,
+ CATALOG_LIST,
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ CATALOG_READ_PROPERTIES,
+ CATALOG_WRITE_PROPERTIES,
+ SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_READ_PROPERTIES,
+ List.of(
+ CATALOG_FULL_METADATA,
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ CATALOG_READ_PROPERTIES,
+ CATALOG_WRITE_PROPERTIES,
+ SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_WRITE_PROPERTIES,
+ List.of(
+ CATALOG_FULL_METADATA,
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ CATALOG_WRITE_PROPERTIES,
+ SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_FULL_METADATA, List.of(CATALOG_FULL_METADATA, SERVICE_MANAGE_ACCESS));
+
+ // _LIST_GRANTS
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_LIST_GRANTS,
+ List.of(
+ PRINCIPAL_LIST_GRANTS,
+ PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE,
+ PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE,
+ SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_ROLE_LIST_GRANTS,
+ List.of(
+ PRINCIPAL_ROLE_LIST_GRANTS,
+ PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE,
+ PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE,
+ SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_ROLE_LIST_GRANTS,
+ List.of(
+ CATALOG_ROLE_LIST_GRANTS,
+ CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE,
+ CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE,
+ CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_LIST_GRANTS,
+ List.of(CATALOG_LIST_GRANTS, CATALOG_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ NAMESPACE_LIST_GRANTS,
+ List.of(
+ NAMESPACE_LIST_GRANTS, NAMESPACE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_LIST_GRANTS,
+ List.of(TABLE_LIST_GRANTS, TABLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ VIEW_LIST_GRANTS,
+ List.of(VIEW_LIST_GRANTS, VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
+
+ // _MANAGE_GRANTS_ON_SECURABLE for CATALOG, NAMESPACE, TABLE, VIEW
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_MANAGE_GRANTS_ON_SECURABLE,
+ List.of(CATALOG_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ NAMESPACE_MANAGE_GRANTS_ON_SECURABLE,
+ List.of(NAMESPACE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_MANAGE_GRANTS_ON_SECURABLE,
+ List.of(TABLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ VIEW_MANAGE_GRANTS_ON_SECURABLE,
+ List.of(VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
+
+ // PRINCIPAL CRUDL
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_CREATE,
+ List.of(PRINCIPAL_CREATE, PRINCIPAL_FULL_METADATA, SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_DROP, List.of(PRINCIPAL_DROP, PRINCIPAL_FULL_METADATA, SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_LIST,
+ List.of(
+ PRINCIPAL_LIST,
+ PRINCIPAL_CREATE,
+ PRINCIPAL_READ_PROPERTIES,
+ PRINCIPAL_WRITE_PROPERTIES,
+ PRINCIPAL_FULL_METADATA,
+ SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_READ_PROPERTIES,
+ List.of(
+ PRINCIPAL_READ_PROPERTIES,
+ PRINCIPAL_WRITE_PROPERTIES,
+ PRINCIPAL_FULL_METADATA,
+ SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_WRITE_PROPERTIES,
+ List.of(PRINCIPAL_WRITE_PROPERTIES, PRINCIPAL_FULL_METADATA, SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_FULL_METADATA, List.of(PRINCIPAL_FULL_METADATA, SERVICE_MANAGE_ACCESS));
+
+ // PRINCIPAL MANAGE_GRANTS
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE,
+ List.of(PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE, SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE,
+ List.of(PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE, SERVICE_MANAGE_ACCESS));
+
+ // PRINCIPAL special privileges
+ SUPER_PRIVILEGES.putAll(PRINCIPAL_ROTATE_CREDENTIALS, List.of(PRINCIPAL_ROTATE_CREDENTIALS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_RESET_CREDENTIALS, List.of(PRINCIPAL_RESET_CREDENTIALS, SERVICE_MANAGE_ACCESS));
+
+ // PRINCIPAL_ROLE CRUDL
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_ROLE_CREATE,
+ List.of(PRINCIPAL_ROLE_CREATE, PRINCIPAL_ROLE_FULL_METADATA, SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_ROLE_DROP,
+ List.of(PRINCIPAL_ROLE_DROP, PRINCIPAL_ROLE_FULL_METADATA, SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_ROLE_LIST,
+ List.of(
+ PRINCIPAL_ROLE_LIST,
+ PRINCIPAL_ROLE_CREATE,
+ PRINCIPAL_ROLE_READ_PROPERTIES,
+ PRINCIPAL_ROLE_WRITE_PROPERTIES,
+ PRINCIPAL_ROLE_FULL_METADATA,
+ SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_ROLE_READ_PROPERTIES,
+ List.of(
+ PRINCIPAL_ROLE_READ_PROPERTIES,
+ PRINCIPAL_ROLE_WRITE_PROPERTIES,
+ PRINCIPAL_ROLE_FULL_METADATA,
+ SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_ROLE_WRITE_PROPERTIES,
+ List.of(
+ PRINCIPAL_ROLE_WRITE_PROPERTIES, PRINCIPAL_ROLE_FULL_METADATA, SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_ROLE_FULL_METADATA, List.of(PRINCIPAL_ROLE_FULL_METADATA, SERVICE_MANAGE_ACCESS));
+
+ // PRINCIPAL_ROLE_ROLE MANAGE_GRANTS
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE,
+ List.of(PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, SERVICE_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE,
+ List.of(PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE, SERVICE_MANAGE_ACCESS));
+
+ // CATALOG_ROLE CRUDL
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_ROLE_CREATE,
+ List.of(CATALOG_ROLE_CREATE, CATALOG_ROLE_FULL_METADATA, CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_ROLE_DROP,
+ List.of(CATALOG_ROLE_DROP, CATALOG_ROLE_FULL_METADATA, CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_ROLE_LIST,
+ List.of(
+ CATALOG_ROLE_LIST,
+ CATALOG_ROLE_CREATE,
+ CATALOG_ROLE_READ_PROPERTIES,
+ CATALOG_ROLE_WRITE_PROPERTIES,
+ CATALOG_ROLE_FULL_METADATA,
+ CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_ROLE_READ_PROPERTIES,
+ List.of(
+ CATALOG_ROLE_READ_PROPERTIES,
+ CATALOG_ROLE_WRITE_PROPERTIES,
+ CATALOG_ROLE_FULL_METADATA,
+ CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_ROLE_WRITE_PROPERTIES,
+ List.of(CATALOG_ROLE_WRITE_PROPERTIES, CATALOG_ROLE_FULL_METADATA, CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_ROLE_FULL_METADATA, List.of(CATALOG_ROLE_FULL_METADATA, CATALOG_MANAGE_ACCESS));
+
+ // CATALOG_ROLE_ROLE MANAGE_GRANTS
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE,
+ List.of(CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
+ SUPER_PRIVILEGES.putAll(
+ CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE,
+ List.of(CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE, CATALOG_MANAGE_ACCESS));
+ }
+
+ private final PolarisConfigurationStore featureConfig;
+
+ public PolarisAuthorizer(PolarisConfigurationStore featureConfig) {
+ this.featureConfig = featureConfig;
+ }
+
+ /**
+ * Checks whether the {@code grantedPrivilege} is sufficient to confer {@code desiredPrivilege},
+ * assuming the privileges are referring to the same securable object. In other words, whether the
+ * grantedPrivilege is "better than or equal to" the desiredPrivilege.
+ */
+ public boolean matchesOrIsSubsumedBy(
+ PolarisPrivilege desiredPrivilege, PolarisPrivilege grantedPrivilege) {
+ if (grantedPrivilege == desiredPrivilege) {
+ return true;
+ }
+
+ if (SUPER_PRIVILEGES.containsKey(desiredPrivilege)
+ && SUPER_PRIVILEGES.get(desiredPrivilege).contains(grantedPrivilege)) {
+ return true;
+ }
+ // TODO: Fill out the map, maybe in the PolarisPrivilege enum definition itself.
+ return false;
+ }
+
+ public void authorizeOrThrow(
+ @NotNull AuthenticatedPolarisPrincipal authenticatedPrincipal,
+ @NotNull Set activatedGranteeIds,
+ @NotNull PolarisAuthorizableOperation authzOp,
+ @Nullable PolarisResolvedPathWrapper target,
+ @Nullable PolarisResolvedPathWrapper secondary) {
+ authorizeOrThrow(
+ authenticatedPrincipal,
+ activatedGranteeIds,
+ authzOp,
+ target == null ? null : List.of(target),
+ secondary == null ? null : List.of(secondary));
+ }
+
+ public void authorizeOrThrow(
+ @NotNull AuthenticatedPolarisPrincipal authenticatedPrincipal,
+ @NotNull Set activatedGranteeIds,
+ @NotNull PolarisAuthorizableOperation authzOp,
+ @Nullable List targets,
+ @Nullable List secondaries) {
+ boolean enforceCredentialRotationRequiredState =
+ featureConfig.getConfiguration(
+ CallContext.getCurrentContext().getPolarisCallContext(),
+ PolarisConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING,
+ false);
+ if (enforceCredentialRotationRequiredState
+ && authenticatedPrincipal
+ .getPrincipalEntity()
+ .getInternalPropertiesAsMap()
+ .containsKey(PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)
+ && authzOp != PolarisAuthorizableOperation.ROTATE_CREDENTIALS) {
+ throw new ForbiddenException(
+ "Principal '%s' is not authorized for op %s due to PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE",
+ authenticatedPrincipal.getName(), authzOp);
+ } else if (!isAuthorized(
+ authenticatedPrincipal, activatedGranteeIds, authzOp, targets, secondaries)) {
+ throw new ForbiddenException(
+ "Principal '%s' with activated PrincipalRoles '%s' and activated ids '%s' is not authorized for op %s",
+ authenticatedPrincipal.getName(),
+ authenticatedPrincipal.getActivatedPrincipalRoleNames(),
+ activatedGranteeIds,
+ authzOp);
+ }
+ }
+
+ /**
+ * Based on the required target/targetParent/secondary/secondaryParent privileges mapped from
+ * {@code authzOp}, determines whether the caller's set of activatedGranteeIds is authorized for
+ * the operation.
+ */
+ public boolean isAuthorized(
+ @NotNull AuthenticatedPolarisPrincipal authenticatedPolarisPrincipal,
+ @NotNull Set activatedGranteeIds,
+ @NotNull PolarisAuthorizableOperation authzOp,
+ @Nullable PolarisResolvedPathWrapper target,
+ @Nullable PolarisResolvedPathWrapper secondary) {
+ return isAuthorized(
+ authenticatedPolarisPrincipal,
+ activatedGranteeIds,
+ authzOp,
+ target == null ? null : List.of(target),
+ secondary == null ? null : List.of(secondary));
+ }
+
+ public boolean isAuthorized(
+ @NotNull AuthenticatedPolarisPrincipal authenticatedPolarisPrincipal,
+ @NotNull Set activatedGranteeIds,
+ @NotNull PolarisAuthorizableOperation authzOp,
+ @Nullable List targets,
+ @Nullable List secondaries) {
+ for (PolarisPrivilege privilegeOnTarget : authzOp.getPrivilegesOnTarget()) {
+ // If any privileges are required on target, the target must be non-null.
+ Preconditions.checkState(
+ targets != null,
+ "Got null target when authorizing authzOp %s for privilege %s",
+ authzOp,
+ privilegeOnTarget);
+ for (PolarisResolvedPathWrapper target : targets) {
+ if (!hasTransitivePrivilege(
+ authenticatedPolarisPrincipal, activatedGranteeIds, privilegeOnTarget, target)) {
+ // TODO: Collect missing privileges to report all at the end and/or return to code
+ // that throws NotAuthorizedException for more useful messages.
+ return false;
+ }
+ }
+ }
+ for (PolarisPrivilege privilegeOnSecondary : authzOp.getPrivilegesOnSecondary()) {
+ Preconditions.checkState(
+ secondaries != null,
+ "Got null secondary when authorizing authzOp %s for privilege %s",
+ authzOp,
+ privilegeOnSecondary);
+ for (PolarisResolvedPathWrapper secondary : secondaries) {
+ if (!hasTransitivePrivilege(
+ authenticatedPolarisPrincipal, activatedGranteeIds, privilegeOnSecondary, secondary)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether the resolvedPrincipal in the {@code resolved} resolvedPath has role-expanded
+ * permissions matching {@code privilege} on any entity in the resolvedPath of the resolvedPath.
+ *
+ * The caller is responsible for translating these checks into either behavioral actions (e.g.
+ * returning 404 instead of 403, checking other root privileges that supercede the checked
+ * privilege, choosing whether to vend credentials) or throwing relevant Unauthorized
+ * errors/exceptions.
+ */
+ public boolean hasTransitivePrivilege(
+ @NotNull AuthenticatedPolarisPrincipal authenticatedPolarisPrincipal,
+ Set activatedGranteeIds,
+ PolarisPrivilege desiredPrivilege,
+ PolarisResolvedPathWrapper resolvedPath) {
+
+ // Iterate starting at the parent, since the most common case should be to manage grants as
+ // high up in the resource hierarchy as possible, so we expect earlier termination.
+ for (ResolvedPolarisEntity resolvedSecurableEntity : resolvedPath.getResolvedFullPath()) {
+ Preconditions.checkState(
+ resolvedSecurableEntity.getGrantRecordsAsSecurable() != null,
+ "Got null grantRecordsAsSecurable for resolvedSecurableEntity %s",
+ resolvedSecurableEntity);
+ for (PolarisGrantRecord grantRecord : resolvedSecurableEntity.getGrantRecordsAsSecurable()) {
+ if (matchesOrIsSubsumedBy(
+ desiredPrivilege, PolarisPrivilege.fromCode(grantRecord.getPrivilegeCode()))) {
+ // Found a potential candidate for satisfying our authz goal.
+ if (activatedGranteeIds.contains(grantRecord.getGranteeId())) {
+ LOG.debug(
+ "Satisfied privilege {} with grantRecord {} from securable {} for "
+ + "principalName {} and activatedIds {}",
+ desiredPrivilege,
+ grantRecord,
+ resolvedSecurableEntity,
+ authenticatedPolarisPrincipal.getName(),
+ activatedGranteeIds);
+ return true;
+ }
+ }
+ }
+ }
+
+ LOG.debug(
+ "Failed to satisfy privilege {} for principalName {} on resolvedPath {}",
+ desiredPrivilege,
+ authenticatedPolarisPrincipal.getName(),
+ resolvedPath);
+ return false;
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/catalog/PolarisCatalogHelpers.java b/polaris-core/src/main/java/io/polaris/core/catalog/PolarisCatalogHelpers.java
new file mode 100644
index 0000000000..cf3e94052d
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/catalog/PolarisCatalogHelpers.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.catalog;
+
+import io.polaris.core.entity.PolarisEntity;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Holds helper methods translating between persistence-layer structs and Iceberg objects shared by
+ * different Polaris components.
+ */
+public class PolarisCatalogHelpers {
+ private static final Logger LOG = LoggerFactory.getLogger(PolarisCatalogHelpers.class);
+
+ /** Not intended for instantiation. */
+ private PolarisCatalogHelpers() {}
+
+ public static List tableIdentifierToList(TableIdentifier identifier) {
+ List fullList = new ArrayList<>();
+ fullList.addAll(Arrays.asList(identifier.namespace().levels()));
+ fullList.add(identifier.name());
+ return fullList;
+ }
+
+ public static TableIdentifier listToTableIdentifier(List ids) {
+ return TableIdentifier.of(ids.toArray(new String[0]));
+ }
+
+ public static Namespace getParentNamespace(Namespace namespace) {
+ if (namespace.isEmpty() || namespace.length() == 1) {
+ return Namespace.empty();
+ }
+ String[] parentLevels = new String[namespace.length() - 1];
+ for (int i = 0; i < parentLevels.length; ++i) {
+ parentLevels[i] = namespace.level(i);
+ }
+ return Namespace.of(parentLevels);
+ }
+
+ public static List nameAndIdToNamespaces(
+ List catalogPath, List entities) {
+ // Skip element 0 which is the catalog entity
+ String[] parentNamespaces = new String[catalogPath.size() - 1];
+ for (int i = 0; i < parentNamespaces.length; ++i) {
+ parentNamespaces[i] = catalogPath.get(i + 1).getName();
+ }
+ List namespaces = new ArrayList<>();
+ for (PolarisEntity.NameAndId entity : entities) {
+ String[] fullName = Arrays.copyOf(parentNamespaces, parentNamespaces.length + 1);
+ fullName[fullName.length - 1] = entity.getName();
+ namespaces.add(Namespace.of(fullName));
+ }
+ return namespaces;
+ }
+
+ /**
+ * Given the shortnames/ids of entities that all live under the given catalogPath, reconstructs
+ * TableIdentifier objects for each that all hold the catalogPath excluding the catalog entity.
+ */
+ public static List nameAndIdToTableIdentifiers(
+ List catalogPath, List entities) {
+ // Skip element 0 which is the catalog entity
+ String[] parentNamespaces = new String[catalogPath.size() - 1];
+ for (int i = 0; i < parentNamespaces.length; ++i) {
+ parentNamespaces[i] = catalogPath.get(i + 1).getName();
+ }
+ Namespace sharedNamespace = Namespace.of(parentNamespaces);
+ return entities.stream()
+ .map(entity -> TableIdentifier.of(sharedNamespace, entity.getName()))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/context/CallContext.java b/polaris-core/src/main/java/io/polaris/core/context/CallContext.java
new file mode 100644
index 0000000000..39940b9340
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/context/CallContext.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.context;
+
+import io.polaris.core.PolarisCallContext;
+import io.polaris.core.PolarisDiagnostics;
+import io.polaris.core.auth.AuthenticatedPolarisPrincipal;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.iceberg.io.CloseableGroup;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Stores elements associated with an individual REST request such as RealmContext, caller
+ * identity/role, authn/authz, etc. This class is distinct from RealmContext because implementations
+ * may need to first independently resolve a RealmContext before resolving the identity/role
+ * elements of the CallContext that reside exclusively within the resolved Realm. For example, the
+ * principal/role entities may be defined within a Realm-specific persistence layer, and the
+ * underlying nature of the persistence layer may differ between different realms.
+ */
+public interface CallContext extends AutoCloseable {
+ InheritableThreadLocal CURRENT_CONTEXT = new InheritableThreadLocal<>();
+
+ // For requests that make use of a Catalog instance, this holds the instance that was
+ // created, scoped to the current call context.
+ public static final String REQUEST_PATH_CATALOG_INSTANCE_KEY = "REQUEST_PATH_CATALOG_INSTANCE";
+
+ // Authenticator filters should populate this field alongside resolving a SecurityContext.
+ // Value type: AuthenticatedPolarisPrincipal
+ String AUTHENTICATED_PRINCIPAL = "AUTHENTICATED_PRINCIPAL";
+ String CLOSEABLES = "closeables";
+
+ static CallContext setCurrentContext(CallContext context) {
+ CURRENT_CONTEXT.set(context);
+ return context;
+ }
+
+ static CallContext getCurrentContext() {
+ return CURRENT_CONTEXT.get();
+ }
+
+ static PolarisDiagnostics getDiagnostics() {
+ return CURRENT_CONTEXT.get().getPolarisCallContext().getDiagServices();
+ }
+
+ static AuthenticatedPolarisPrincipal getAuthenticatedPrincipal() {
+ return (AuthenticatedPolarisPrincipal)
+ CallContext.getCurrentContext().contextVariables().get(CallContext.AUTHENTICATED_PRINCIPAL);
+ }
+
+ static void unsetCurrentContext() {
+ CURRENT_CONTEXT.remove();
+ }
+
+ static CallContext of(
+ final RealmContext realmContext, final PolarisCallContext polarisCallContext) {
+ Map map = new HashMap<>();
+ return new CallContext() {
+ @Override
+ public RealmContext getRealmContext() {
+ return realmContext;
+ }
+
+ @Override
+ public PolarisCallContext getPolarisCallContext() {
+ return polarisCallContext;
+ }
+
+ @Override
+ public Map contextVariables() {
+ return map;
+ }
+ };
+ }
+
+ /**
+ * Copy the {@link CallContext}. {@link #contextVariables()} will be copied except for {@link
+ * #closeables()}. The original {@link #contextVariables()} map is untouched and {@link
+ * #closeables()} in the original {@link CallContext} should be closed along with the {@link
+ * CallContext}.
+ *
+ * @param base
+ * @return
+ */
+ static CallContext copyOf(CallContext base) {
+ RealmContext realmContext = base.getRealmContext();
+ PolarisCallContext polarisCallContext = base.getPolarisCallContext();
+ Map contextVariables =
+ base.contextVariables().entrySet().stream()
+ .filter(e -> !e.getKey().equals(CLOSEABLES))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ return new CallContext() {
+ @Override
+ public RealmContext getRealmContext() {
+ return realmContext;
+ }
+
+ @Override
+ public PolarisCallContext getPolarisCallContext() {
+ return polarisCallContext;
+ }
+
+ @Override
+ public Map contextVariables() {
+ return contextVariables;
+ }
+ };
+ }
+
+ RealmContext getRealmContext();
+
+ /**
+ * @return the inner context used for delegating services
+ */
+ PolarisCallContext getPolarisCallContext();
+
+ Map contextVariables();
+
+ default @NotNull CloseableGroup closeables() {
+ return (CloseableGroup)
+ contextVariables().computeIfAbsent(CLOSEABLES, key -> new CloseableGroup());
+ }
+
+ default void close() {
+ if (CURRENT_CONTEXT.get() == this) {
+ unsetCurrentContext();
+ CloseableGroup closeables = closeables();
+ try {
+ closeables.close();
+ } catch (IOException e) {
+ Logger logger = LoggerFactory.getLogger(CallContext.class);
+ logger
+ .atWarn()
+ .addKeyValue("closeableGroup", closeables)
+ .log("Unable to close closeable group", e);
+ }
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/context/RealmContext.java b/polaris-core/src/main/java/io/polaris/core/context/RealmContext.java
new file mode 100644
index 0000000000..ad5f7445e0
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/context/RealmContext.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.context;
+
+/**
+ * Represents the elements of a REST request associated with routing to independent and isolated
+ * "universes". This may include properties such as region, deployment environment (e.g. dev, qa,
+ * prod), and/or account.
+ */
+public interface RealmContext {
+ String getRealmIdentifier();
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/AsyncTaskType.java b/polaris-core/src/main/java/io/polaris/core/entity/AsyncTaskType.java
new file mode 100644
index 0000000000..f710a4dbe9
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/AsyncTaskType.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+public enum AsyncTaskType {
+ ENTITY_CLEANUP_SCHEDULER(1),
+ FILE_CLEANUP(2);
+
+ private final int typeCode;
+
+ AsyncTaskType(int typeCode) {
+ this.typeCode = typeCode;
+ }
+
+ @JsonValue
+ public int typeCode() {
+ return typeCode;
+ }
+
+ @JsonCreator
+ public static AsyncTaskType fromTypeCode(int typeCode) {
+ for (AsyncTaskType taskType : AsyncTaskType.values()) {
+ if (taskType.typeCode == typeCode) {
+ return taskType;
+ }
+ }
+ return null;
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/CatalogEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/CatalogEntity.java
new file mode 100644
index 0000000000..1b01ab6559
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/CatalogEntity.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import static io.polaris.core.admin.model.StorageConfigInfo.StorageTypeEnum.AZURE;
+
+import io.polaris.core.PolarisDefaultDiagServiceImpl;
+import io.polaris.core.admin.model.AwsStorageConfigInfo;
+import io.polaris.core.admin.model.AzureStorageConfigInfo;
+import io.polaris.core.admin.model.Catalog;
+import io.polaris.core.admin.model.CatalogProperties;
+import io.polaris.core.admin.model.ExternalCatalog;
+import io.polaris.core.admin.model.FileStorageConfigInfo;
+import io.polaris.core.admin.model.GcpStorageConfigInfo;
+import io.polaris.core.admin.model.PolarisCatalog;
+import io.polaris.core.admin.model.StorageConfigInfo;
+import io.polaris.core.storage.FileStorageConfigurationInfo;
+import io.polaris.core.storage.PolarisStorageConfigurationInfo;
+import io.polaris.core.storage.aws.AwsStorageConfigurationInfo;
+import io.polaris.core.storage.azure.AzureStorageConfigurationInfo;
+import io.polaris.core.storage.gcp.GcpStorageConfigurationInfo;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import org.apache.iceberg.exceptions.BadRequestException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Catalog specific subclass of the {@link PolarisEntity} that handles conversion from the {@link
+ * Catalog} model to the persistent entity model.
+ */
+public class CatalogEntity extends PolarisEntity {
+ private static final Logger LOG = LoggerFactory.getLogger(CatalogEntity.class);
+
+ public static final long ROOT_CATALOG_ID = 0;
+ public static final String CATALOG_TYPE_PROPERTY = "catalogType";
+
+ // Specifies the object-store base location used for all Table file locations under the
+ // catalog, stored in the "properties" map.
+ public static final String DEFAULT_BASE_LOCATION_KEY = "default-base-location";
+
+ // Specifies a prefix that will be replaced with the catalog's default-base-location whenever
+ // it matches a specified new table or view location. For example, if the catalog base location
+ // is "s3://my-bucket/base/location" and the prefix specified here is "file:/tmp" then any
+ // new table attempting to specify a base location of "file:/tmp/ns1/ns2/table1" will be
+ // translated into "s3://my-bucket/base/location/ns1/ns2/table1".
+ public static final String REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY =
+ "replace-new-location-prefix-with-catalog-default";
+ public static final String REMOTE_URL = "remoteUrl";
+
+ public CatalogEntity(PolarisBaseEntity sourceEntity) {
+ super(sourceEntity);
+ }
+
+ public static CatalogEntity of(PolarisBaseEntity sourceEntity) {
+ if (sourceEntity != null) {
+ return new CatalogEntity(sourceEntity);
+ }
+ return null;
+ }
+
+ public static CatalogEntity fromCatalog(Catalog catalog) {
+
+ Builder builder =
+ new Builder()
+ .setName(catalog.getName())
+ .setProperties(catalog.getProperties().toMap())
+ .setCatalogType(catalog.getType().name());
+ Map internalProperties = new HashMap<>();
+ if (catalog instanceof ExternalCatalog) {
+ internalProperties.put(REMOTE_URL, ((ExternalCatalog) catalog).getRemoteUrl());
+ }
+ internalProperties.put(CATALOG_TYPE_PROPERTY, catalog.getType().name());
+ builder.setInternalProperties(internalProperties);
+ builder.setStorageConfigurationInfo(
+ catalog.getStorageConfigInfo(), getDefaultBaseLocation(catalog));
+ return builder.build();
+ }
+
+ public Catalog asCatalog() {
+ Map internalProperties = getInternalPropertiesAsMap();
+ Catalog.TypeEnum catalogType =
+ Optional.ofNullable(internalProperties.get(CATALOG_TYPE_PROPERTY))
+ .map(Catalog.TypeEnum::valueOf)
+ .orElseGet(() -> getName().equalsIgnoreCase("ROOT") ? Catalog.TypeEnum.INTERNAL : null);
+ Map propertiesMap = getPropertiesAsMap();
+ CatalogProperties catalogProps =
+ CatalogProperties.builder(propertiesMap.get(DEFAULT_BASE_LOCATION_KEY))
+ .putAll(propertiesMap)
+ .build();
+ return catalogType == Catalog.TypeEnum.INTERNAL
+ ? PolarisCatalog.builder()
+ .setType(Catalog.TypeEnum.INTERNAL)
+ .setName(getName())
+ .setProperties(catalogProps)
+ .setCreateTimestamp(getCreateTimestamp())
+ .setLastUpdateTimestamp(getLastUpdateTimestamp())
+ .setEntityVersion(getEntityVersion())
+ .setStorageConfigInfo(getStorageInfo(internalProperties))
+ .build()
+ : ExternalCatalog.builder()
+ .setType(Catalog.TypeEnum.EXTERNAL)
+ .setName(getName())
+ .setRemoteUrl(getInternalPropertiesAsMap().get(REMOTE_URL))
+ .setProperties(catalogProps)
+ .setCreateTimestamp(getCreateTimestamp())
+ .setLastUpdateTimestamp(getLastUpdateTimestamp())
+ .setEntityVersion(getEntityVersion())
+ .setStorageConfigInfo(getStorageInfo(internalProperties))
+ .build();
+ }
+
+ private StorageConfigInfo getStorageInfo(Map internalProperties) {
+ if (internalProperties.containsKey(PolarisEntityConstants.getStorageConfigInfoPropertyName())) {
+ PolarisStorageConfigurationInfo configInfo = getStorageConfigurationInfo();
+ PolarisStorageConfigurationInfo.StorageType storageType = configInfo.getStorageType();
+ if (configInfo instanceof AwsStorageConfigurationInfo) {
+ AwsStorageConfigurationInfo awsConfig = (AwsStorageConfigurationInfo) configInfo;
+ return AwsStorageConfigInfo.builder()
+ .setRoleArn(awsConfig.getRoleARN())
+ .setExternalId(awsConfig.getExternalId())
+ .setUserArn(awsConfig.getUserARN())
+ .setStorageType(StorageConfigInfo.StorageTypeEnum.S3)
+ .setAllowedLocations(awsConfig.getAllowedLocations())
+ .build();
+ }
+ if (configInfo instanceof AzureStorageConfigurationInfo) {
+ AzureStorageConfigurationInfo azureConfig = (AzureStorageConfigurationInfo) configInfo;
+ return AzureStorageConfigInfo.builder()
+ .setTenantId(azureConfig.getTenantId())
+ .setMultiTenantAppName(azureConfig.getMultiTenantAppName())
+ .setConsentUrl(azureConfig.getConsentUrl())
+ .setStorageType(AZURE)
+ .setAllowedLocations(azureConfig.getAllowedLocations())
+ .build();
+ }
+ if (configInfo instanceof GcpStorageConfigurationInfo) {
+ GcpStorageConfigurationInfo gcpConfigModel = (GcpStorageConfigurationInfo) configInfo;
+ return GcpStorageConfigInfo.builder()
+ .setGcsServiceAccount(gcpConfigModel.getGcpServiceAccount())
+ .setStorageType(StorageConfigInfo.StorageTypeEnum.GCS)
+ .setAllowedLocations(gcpConfigModel.getAllowedLocations())
+ .build();
+ }
+ if (configInfo instanceof FileStorageConfigurationInfo) {
+ FileStorageConfigurationInfo fileConfigModel = (FileStorageConfigurationInfo) configInfo;
+ return new FileStorageConfigInfo(
+ StorageConfigInfo.StorageTypeEnum.FILE, fileConfigModel.getAllowedLocations());
+ }
+ return null;
+ }
+ return null;
+ }
+
+ public String getDefaultBaseLocation() {
+ return getPropertiesAsMap().get(DEFAULT_BASE_LOCATION_KEY);
+ }
+
+ public String getReplaceNewLocationPrefixWithCatalogDefault() {
+ return getPropertiesAsMap().get(REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY);
+ }
+
+ public @Nullable PolarisStorageConfigurationInfo getStorageConfigurationInfo() {
+ String configStr =
+ getInternalPropertiesAsMap().get(PolarisEntityConstants.getStorageConfigInfoPropertyName());
+ if (configStr != null) {
+ return PolarisStorageConfigurationInfo.deserialize(
+ new PolarisDefaultDiagServiceImpl(), configStr);
+ }
+ return null;
+ }
+
+ public Catalog.TypeEnum getCatalogType() {
+ return Optional.ofNullable(getInternalPropertiesAsMap().get(CATALOG_TYPE_PROPERTY))
+ .map(Catalog.TypeEnum::valueOf)
+ .orElse(null);
+ }
+
+ public static class Builder extends PolarisEntity.BaseBuilder {
+ public Builder() {
+ super();
+ setType(PolarisEntityType.CATALOG);
+ setCatalogId(PolarisEntityConstants.getNullId());
+ setParentId(PolarisEntityConstants.getRootEntityId());
+ }
+
+ public Builder(CatalogEntity original) {
+ super(original);
+ }
+
+ public Builder setCatalogType(String type) {
+ internalProperties.put(CATALOG_TYPE_PROPERTY, type);
+ return this;
+ }
+
+ public Builder setDefaultBaseLocation(String defaultBaseLocation) {
+ // Note that this member lives in the main 'properties' map rather tha internalProperties.
+ properties.put(DEFAULT_BASE_LOCATION_KEY, defaultBaseLocation);
+ return this;
+ }
+
+ public Builder setReplaceNewLocationPrefixWithCatalogDefault(String value) {
+ // Note that this member lives in the main 'properties' map rather tha internalProperties.
+ properties.put(REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, value);
+ return this;
+ }
+
+ public Builder setStorageConfigurationInfo(
+ StorageConfigInfo storageConfigModel, String defaultBaseLocation) {
+ if (storageConfigModel != null) {
+ PolarisStorageConfigurationInfo config;
+ Set allowedLocations = new HashSet<>(storageConfigModel.getAllowedLocations());
+
+ // TODO: Reconsider whether this should actually just be a check up-front or if we
+ // actually want to silently add to the allowed locations. Maybe ideally we only
+ // add to the allowedLocations if allowedLocations is empty for the simple case,
+ // but if the caller provided allowedLocations explicitly, then we just verify that
+ // the defaultBaseLocation is at least a subpath of one of the allowedLocations.
+ if (defaultBaseLocation == null) {
+ throw new BadRequestException("Must specify default base location");
+ }
+ allowedLocations.add(defaultBaseLocation);
+ switch (storageConfigModel.getStorageType()) {
+ case S3:
+ AwsStorageConfigInfo awsConfigModel = (AwsStorageConfigInfo) storageConfigModel;
+ config =
+ new AwsStorageConfigurationInfo(
+ PolarisStorageConfigurationInfo.StorageType.S3,
+ new ArrayList<>(allowedLocations),
+ awsConfigModel.getRoleArn(),
+ awsConfigModel.getExternalId());
+ ((AwsStorageConfigurationInfo) config).validateArn(awsConfigModel.getRoleArn());
+ break;
+ case AZURE:
+ AzureStorageConfigInfo azureConfigModel = (AzureStorageConfigInfo) storageConfigModel;
+ config =
+ new AzureStorageConfigurationInfo(
+ new ArrayList<>(allowedLocations), azureConfigModel.getTenantId());
+ break;
+ case GCS:
+ config = new GcpStorageConfigurationInfo(new ArrayList<>(allowedLocations));
+ break;
+ case FILE:
+ config = new FileStorageConfigurationInfo(new ArrayList<>(allowedLocations));
+ break;
+ default:
+ throw new IllegalStateException(
+ "Unsupported storage type: " + storageConfigModel.getStorageType());
+ }
+ internalProperties.put(
+ PolarisEntityConstants.getStorageConfigInfoPropertyName(), config.serialize());
+ }
+ return this;
+ }
+
+ public CatalogEntity build() {
+ return new CatalogEntity(buildBase());
+ }
+ }
+
+ protected static @NotNull String getDefaultBaseLocation(Catalog catalog) {
+ return catalog.getProperties().getDefaultBaseLocation();
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/CatalogRoleEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/CatalogRoleEntity.java
new file mode 100644
index 0000000000..043e1bb285
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/CatalogRoleEntity.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import io.polaris.core.admin.model.CatalogRole;
+
+/** Wrapper for translating between the REST CatalogRole object and the base PolarisEntity type. */
+public class CatalogRoleEntity extends PolarisEntity {
+ public CatalogRoleEntity(PolarisBaseEntity sourceEntity) {
+ super(sourceEntity);
+ }
+
+ public static CatalogRoleEntity of(PolarisBaseEntity sourceEntity) {
+ if (sourceEntity != null) {
+ return new CatalogRoleEntity(sourceEntity);
+ }
+ return null;
+ }
+
+ public static CatalogRoleEntity fromCatalogRole(CatalogRole catalogRole) {
+ return new Builder()
+ .setName(catalogRole.getName())
+ .setProperties(catalogRole.getProperties())
+ .build();
+ }
+
+ public CatalogRole asCatalogRole() {
+ CatalogRole catalogRole =
+ new CatalogRole(
+ getName(),
+ getPropertiesAsMap(),
+ getCreateTimestamp(),
+ getLastUpdateTimestamp(),
+ getEntityVersion());
+ return catalogRole;
+ }
+
+ public static class Builder extends PolarisEntity.BaseBuilder {
+ public Builder() {
+ super();
+ setType(PolarisEntityType.CATALOG_ROLE);
+ }
+
+ public Builder(CatalogRoleEntity original) {
+ super(original);
+ }
+
+ public CatalogRoleEntity build() {
+ return new CatalogRoleEntity(buildBase());
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java
new file mode 100644
index 0000000000..523b47cb42
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.polaris.core.catalog.PolarisCatalogHelpers;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.rest.RESTUtil;
+
+/**
+ * Namespace-specific subclass of the {@link PolarisEntity} that provides accessors interacting with
+ * internalProperties specific to the NAMESPACE type.
+ */
+public class NamespaceEntity extends PolarisEntity {
+ // RESTUtil-encoded parent namespace.
+ public static final String PARENT_NAMESPACE_KEY = "parent-namespace";
+
+ public NamespaceEntity(PolarisBaseEntity sourceEntity) {
+ super(sourceEntity);
+ }
+
+ public static NamespaceEntity of(PolarisBaseEntity sourceEntity) {
+ if (sourceEntity != null) {
+ return new NamespaceEntity(sourceEntity);
+ }
+ return null;
+ }
+
+ public Namespace getParentNamespace() {
+ String encodedNamespace = getInternalPropertiesAsMap().get(PARENT_NAMESPACE_KEY);
+ if (encodedNamespace == null) {
+ return Namespace.empty();
+ }
+ return RESTUtil.decodeNamespace(encodedNamespace);
+ }
+
+ public Namespace asNamespace() {
+ Namespace parent = getParentNamespace();
+ String[] levels = new String[parent.length() + 1];
+ for (int i = 0; i < parent.length(); ++i) {
+ levels[i] = parent.level(i);
+ }
+ levels[levels.length - 1] = getName();
+ return Namespace.of(levels);
+ }
+
+ @JsonIgnore
+ public String getBaseLocation() {
+ return getPropertiesAsMap().get(PolarisEntityConstants.ENTITY_BASE_LOCATION);
+ }
+
+ public static class Builder extends PolarisEntity.BaseBuilder {
+ public Builder(Namespace namespace) {
+ super();
+ setType(PolarisEntityType.NAMESPACE);
+ setParentNamespace(PolarisCatalogHelpers.getParentNamespace(namespace));
+ setName(namespace.level(namespace.length() - 1));
+ }
+
+ public Builder setBaseLocation(String baseLocation) {
+ properties.put(PolarisEntityConstants.ENTITY_BASE_LOCATION, baseLocation);
+ return this;
+ }
+
+ public Builder setParentNamespace(Namespace namespace) {
+ if (namespace != null && !namespace.isEmpty()) {
+ internalProperties.put(PARENT_NAMESPACE_KEY, RESTUtil.encodeNamespace(namespace));
+ }
+ return this;
+ }
+
+ public NamespaceEntity build() {
+ return new NamespaceEntity(buildBase());
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisBaseEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisBaseEntity.java
new file mode 100644
index 0000000000..0209c9951c
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisBaseEntity.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Base polaris entity representing all attributes of a Polaris Entity. This is used to exchange
+ * full entity information between the client and the GS backend
+ */
+public class PolarisBaseEntity extends PolarisEntityCore {
+
+ public static final String EMPTY_MAP_STRING = "{}";
+
+ // to serialize/deserialize properties
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ // the type of the entity when it was resolved
+ protected int subTypeCode;
+
+ // timestamp when this entity was created
+ protected long createTimestamp;
+
+ // when this entity was dropped. Null if was never dropped
+ protected long dropTimestamp;
+
+ // when did we start purging this entity. When not null, un-drop is no longer possible
+ protected long purgeTimestamp;
+
+ // when should we start purging this entity
+ protected long toPurgeTimestamp;
+
+ // last time this entity was updated, only for troubleshooting
+ protected long lastUpdateTimestamp;
+
+ // properties, serialized as a JSON string
+ protected String properties;
+
+ // internal properties, serialized as a JSON string
+ protected String internalProperties;
+
+ // current version for that entity, will be monotonically incremented
+ protected int grantRecordsVersion;
+
+ public int getSubTypeCode() {
+ return subTypeCode;
+ }
+
+ public void setSubTypeCode(int subTypeCode) {
+ this.subTypeCode = subTypeCode;
+ }
+
+ public long getCreateTimestamp() {
+ return createTimestamp;
+ }
+
+ public void setCreateTimestamp(long createTimestamp) {
+ this.createTimestamp = createTimestamp;
+ }
+
+ public long getDropTimestamp() {
+ return dropTimestamp;
+ }
+
+ public void setDropTimestamp(long dropTimestamp) {
+ this.dropTimestamp = dropTimestamp;
+ }
+
+ public long getPurgeTimestamp() {
+ return purgeTimestamp;
+ }
+
+ public void setPurgeTimestamp(long purgeTimestamp) {
+ this.purgeTimestamp = purgeTimestamp;
+ }
+
+ public long getToPurgeTimestamp() {
+ return toPurgeTimestamp;
+ }
+
+ public void setToPurgeTimestamp(long toPurgeTimestamp) {
+ this.toPurgeTimestamp = toPurgeTimestamp;
+ }
+
+ public long getLastUpdateTimestamp() {
+ return lastUpdateTimestamp;
+ }
+
+ public void setLastUpdateTimestamp(long lastUpdateTimestamp) {
+ this.lastUpdateTimestamp = lastUpdateTimestamp;
+ }
+
+ public String getProperties() {
+ return properties != null ? properties : EMPTY_MAP_STRING;
+ }
+
+ @JsonIgnore
+ public Map getPropertiesAsMap() {
+ if (properties == null) {
+ return new HashMap<>();
+ }
+ try {
+ return MAPPER.readValue(properties, new TypeReference<>() {});
+ } catch (JsonProcessingException ex) {
+ throw new IllegalStateException(
+ String.format("Failed to deserialize json. properties %s", properties), ex);
+ }
+ }
+
+ /**
+ * Set one single property
+ *
+ * @param propName name of the property
+ * @param propValue value of that property
+ */
+ public void addProperty(String propName, String propValue) {
+ Map props = this.getPropertiesAsMap();
+ props.put(propName, propValue);
+ this.setPropertiesAsMap(props);
+ }
+
+ public void setProperties(String properties) {
+ this.properties = properties;
+ }
+
+ @JsonIgnore
+ public void setPropertiesAsMap(Map properties) {
+ try {
+ this.properties = properties == null ? null : MAPPER.writeValueAsString(properties);
+ } catch (JsonProcessingException ex) {
+ throw new IllegalStateException(
+ String.format("Failed to serialize json. properties %s", properties), ex);
+ }
+ }
+
+ public String getInternalProperties() {
+ return internalProperties != null ? internalProperties : EMPTY_MAP_STRING;
+ }
+
+ @JsonIgnore
+ public Map getInternalPropertiesAsMap() {
+ if (this.internalProperties == null) {
+ return new HashMap<>();
+ }
+ try {
+ return MAPPER.readValue(this.internalProperties, new TypeReference<>() {});
+ } catch (JsonProcessingException ex) {
+ throw new IllegalStateException(
+ String.format(
+ "Failed to deserialize json. internalProperties %s", this.internalProperties),
+ ex);
+ }
+ }
+
+ /**
+ * Set one single internal property
+ *
+ * @param propName name of the property
+ * @param propValue value of that property
+ */
+ public void addInternalProperty(String propName, String propValue) {
+ Map props = this.getInternalPropertiesAsMap();
+ props.put(propName, propValue);
+ this.setInternalPropertiesAsMap(props);
+ }
+
+ public void setInternalProperties(String internalProperties) {
+ this.internalProperties = internalProperties;
+ }
+
+ @JsonIgnore
+ public void setInternalPropertiesAsMap(Map internalProperties) {
+ try {
+ this.internalProperties =
+ internalProperties == null ? null : MAPPER.writeValueAsString(internalProperties);
+ } catch (JsonProcessingException ex) {
+ throw new IllegalStateException(
+ String.format("Failed to serialize json. internalProperties %s", internalProperties), ex);
+ }
+ }
+
+ public int getGrantRecordsVersion() {
+ return grantRecordsVersion;
+ }
+
+ public void setGrantRecordsVersion(int grantRecordsVersion) {
+ this.grantRecordsVersion = grantRecordsVersion;
+ }
+
+ public static PolarisBaseEntity fromCore(
+ PolarisEntityCore coreEntity, PolarisEntityType entityType, PolarisEntitySubType subType) {
+ return new PolarisBaseEntity(
+ coreEntity.getCatalogId(),
+ coreEntity.getId(),
+ entityType,
+ subType,
+ coreEntity.getParentId(),
+ coreEntity.getName());
+ }
+
+ /**
+ * Copy constructor
+ *
+ * @param entity entity to copy
+ */
+ public PolarisBaseEntity(PolarisBaseEntity entity) {
+ super(
+ entity.getCatalogId(),
+ entity.getId(),
+ entity.getParentId(),
+ entity.getTypeCode(),
+ entity.getName(),
+ entity.getEntityVersion());
+ this.subTypeCode = entity.getSubTypeCode();
+ this.createTimestamp = entity.getCreateTimestamp();
+ this.dropTimestamp = entity.getDropTimestamp();
+ this.purgeTimestamp = entity.getPurgeTimestamp();
+ this.toPurgeTimestamp = entity.getToPurgeTimestamp();
+ this.lastUpdateTimestamp = entity.getLastUpdateTimestamp();
+ this.properties = entity.getProperties();
+ this.internalProperties = entity.getInternalProperties();
+ this.grantRecordsVersion = entity.getGrantRecordsVersion();
+ }
+
+ /** Build the DTO for a new entity */
+ public PolarisBaseEntity(
+ long catalogId,
+ long id,
+ PolarisEntityType type,
+ PolarisEntitySubType subType,
+ long parentId,
+ String name) {
+ this(catalogId, id, type.getCode(), subType.getCode(), parentId, name);
+ }
+
+ /** Build the DTO for a new entity */
+ protected PolarisBaseEntity(
+ long catalogId, long id, int typeCode, int subTypeCode, long parentId, String name) {
+ super(catalogId, id, parentId, typeCode, name, 1);
+ this.subTypeCode = subTypeCode;
+ this.createTimestamp = System.currentTimeMillis();
+ this.dropTimestamp = 0;
+ this.purgeTimestamp = 0;
+ this.toPurgeTimestamp = 0;
+ this.lastUpdateTimestamp = this.createTimestamp;
+ this.properties = null;
+ this.internalProperties = null;
+ this.grantRecordsVersion = 1;
+ }
+
+ /** Build the DTO for a new entity */
+ protected PolarisBaseEntity() {
+ super();
+ }
+
+ /**
+ * @return the subtype of this entity
+ */
+ public @JsonIgnore PolarisEntitySubType getSubType() {
+ return PolarisEntitySubType.fromCode(this.subTypeCode);
+ }
+
+ /**
+ * @return true if this entity has been dropped
+ */
+ public @JsonIgnore boolean isDropped() {
+ return this.dropTimestamp != 0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!super.equals(o)) {
+ return false;
+ }
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PolarisBaseEntity)) {
+ return false;
+ }
+ PolarisBaseEntity that = (PolarisBaseEntity) o;
+ return subTypeCode == that.subTypeCode
+ && createTimestamp == that.createTimestamp
+ && dropTimestamp == that.dropTimestamp
+ && purgeTimestamp == that.purgeTimestamp
+ && toPurgeTimestamp == that.toPurgeTimestamp
+ && lastUpdateTimestamp == that.lastUpdateTimestamp
+ && grantRecordsVersion == that.grantRecordsVersion
+ && Objects.equals(properties, that.properties)
+ && Objects.equals(internalProperties, that.internalProperties);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ catalogId,
+ id,
+ parentId,
+ typeCode,
+ name,
+ entityVersion,
+ subTypeCode,
+ createTimestamp,
+ dropTimestamp,
+ purgeTimestamp,
+ toPurgeTimestamp,
+ lastUpdateTimestamp,
+ properties,
+ internalProperties,
+ grantRecordsVersion);
+ }
+
+ @Override
+ public String toString() {
+ return "PolarisBaseEntity{"
+ + super.toString()
+ + ", subTypeCode="
+ + subTypeCode
+ + ", createTimestamp="
+ + createTimestamp
+ + ", dropTimestamp="
+ + dropTimestamp
+ + ", purgeTimestamp="
+ + purgeTimestamp
+ + ", toPurgeTimestamp="
+ + toPurgeTimestamp
+ + ", lastUpdateTimestamp="
+ + lastUpdateTimestamp
+ + ", grantRecordsVersion="
+ + grantRecordsVersion
+ + '}';
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisChangeTrackingVersions.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisChangeTrackingVersions.java
new file mode 100644
index 0000000000..09808aa285
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisChangeTrackingVersions.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/** Simple class to represent the version and grant records version associated to an entity */
+public class PolarisChangeTrackingVersions {
+ // entity version
+ private final int entityVersion;
+
+ // entity grant records version
+ private final int grantRecordsVersion;
+
+ /**
+ * Constructor
+ *
+ * @param entityVersion entity version
+ * @param grantRecordsVersion entity grant records version
+ */
+ @JsonCreator
+ public PolarisChangeTrackingVersions(
+ @JsonProperty("entityVersion") int entityVersion,
+ @JsonProperty("grantRecordsVersion") int grantRecordsVersion) {
+ this.entityVersion = entityVersion;
+ this.grantRecordsVersion = grantRecordsVersion;
+ }
+
+ public int getEntityVersion() {
+ return entityVersion;
+ }
+
+ public int getGrantRecordsVersion() {
+ return grantRecordsVersion;
+ }
+
+ @Override
+ public String toString() {
+ return "PolarisChangeTrackingVersions{"
+ + "entityVersion="
+ + entityVersion
+ + ", grantRecordsVersion="
+ + grantRecordsVersion
+ + '}';
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitiesActiveKey.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitiesActiveKey.java
new file mode 100644
index 0000000000..2d8d3a8d64
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitiesActiveKey.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+public class PolarisEntitiesActiveKey {
+
+ // entity catalog id
+ private final long catalogId;
+
+ // parent id of the entity
+ private final long parentId;
+
+ // code representing the type of that entity
+ private final int typeCode;
+
+ // name of the entity
+ private final String name;
+
+ public PolarisEntitiesActiveKey(long catalogId, long parentId, int typeCode, String name) {
+ this.catalogId = catalogId;
+ this.parentId = parentId;
+ this.typeCode = typeCode;
+ this.name = name;
+ }
+
+ /** Constructor to create the object with provided entity */
+ public PolarisEntitiesActiveKey(PolarisEntityCore entity) {
+ this.catalogId = entity.getCatalogId();
+ this.parentId = entity.getParentId();
+ this.typeCode = entity.getTypeCode();
+ this.name = entity.getName();
+ }
+
+ public long getCatalogId() {
+ return catalogId;
+ }
+
+ public long getParentId() {
+ return parentId;
+ }
+
+ public int getTypeCode() {
+ return typeCode;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return "PolarisEntitiesActiveKey{"
+ + "catalogId="
+ + catalogId
+ + ", parentId="
+ + parentId
+ + ", typeCode="
+ + typeCode
+ + ", name='"
+ + name
+ + '\''
+ + '}';
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java
new file mode 100644
index 0000000000..9dbf8b97d9
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.polaris.core.persistence.PolarisMetaStoreManager;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.NotNull;
+
+public class PolarisEntity extends PolarisBaseEntity {
+
+ public static class NameAndId {
+ private final String name;
+ private final long id;
+
+ public NameAndId(String name, long id) {
+ this.name = name;
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public long getId() {
+ return id;
+ }
+ }
+
+ public static class TypeSubTypeAndName {
+ private final PolarisEntityType type;
+ private final PolarisEntitySubType subType;
+ private final String name;
+
+ public TypeSubTypeAndName(PolarisEntityType type, PolarisEntitySubType subType, String name) {
+ this.type = type;
+ this.subType = subType;
+ this.name = name;
+ }
+
+ public PolarisEntityType getType() {
+ return type;
+ }
+
+ public PolarisEntitySubType getSubType() {
+ return subType;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ @JsonCreator
+ private PolarisEntity(
+ @JsonProperty("catalogId") long catalogId,
+ @JsonProperty("typeCode") PolarisEntityType type,
+ @JsonProperty("subTypeCode") PolarisEntitySubType subType,
+ @JsonProperty("id") long id,
+ @JsonProperty("parentId") long parentId,
+ @JsonProperty("name") String name,
+ @JsonProperty("createTimestamp") long createTimestamp,
+ @JsonProperty("dropTimestamp") long dropTimestamp,
+ @JsonProperty("purgeTimestamp") long purgeTimestamp,
+ @JsonProperty("lastUpdateTimestamp") long lastUpdateTimestamp,
+ @JsonProperty("properties") String properties,
+ @JsonProperty("internalProperties") String internalProperties,
+ @JsonProperty("entityVersion") int entityVersion,
+ @JsonProperty("grantRecordsVersion") int grantRecordsVersion) {
+ super(catalogId, id, type, subType, parentId, name);
+ this.createTimestamp = createTimestamp;
+ this.dropTimestamp = dropTimestamp;
+ this.purgeTimestamp = purgeTimestamp;
+ this.lastUpdateTimestamp = lastUpdateTimestamp;
+ this.properties = properties;
+ this.internalProperties = internalProperties;
+ this.entityVersion = entityVersion;
+ this.grantRecordsVersion = grantRecordsVersion;
+ }
+
+ public PolarisEntity(
+ long catalogId,
+ PolarisEntityType type,
+ PolarisEntitySubType subType,
+ long id,
+ long parentId,
+ String name,
+ long createTimestamp,
+ long dropTimestamp,
+ long purgeTimestamp,
+ long lastUpdateTimestamp,
+ Map properties,
+ Map internalProperties,
+ int entityVersion,
+ int grantRecordsVersion) {
+ super(catalogId, id, type, subType, parentId, name);
+ this.createTimestamp = createTimestamp;
+ this.dropTimestamp = dropTimestamp;
+ this.purgeTimestamp = purgeTimestamp;
+ this.lastUpdateTimestamp = lastUpdateTimestamp;
+ this.setPropertiesAsMap(properties);
+ this.setInternalPropertiesAsMap(internalProperties);
+ this.entityVersion = entityVersion;
+ this.grantRecordsVersion = grantRecordsVersion;
+ }
+
+ public static PolarisEntity of(PolarisBaseEntity sourceEntity) {
+ if (sourceEntity != null) {
+ return new PolarisEntity(sourceEntity);
+ }
+ return null;
+ }
+
+ public static PolarisEntity of(PolarisMetaStoreManager.EntityResult result) {
+ if (result.isSuccess()) {
+ return new PolarisEntity(result.getEntity());
+ }
+ return null;
+ }
+
+ public static PolarisEntityCore toCore(PolarisBaseEntity entity) {
+ PolarisEntityCore entityCore =
+ new PolarisEntityCore(
+ entity.getCatalogId(),
+ entity.getId(),
+ entity.getParentId(),
+ entity.getTypeCode(),
+ entity.getName(),
+ entity.getEntityVersion());
+ return entityCore;
+ }
+
+ public static List toCoreList(List path) {
+ return Optional.ofNullable(path)
+ .filter(Predicate.not(List::isEmpty))
+ .map(list -> list.stream().map(PolarisEntity::toCore).collect(Collectors.toList()))
+ .orElse(null);
+ }
+
+ public static List toNameAndIdList(List entities) {
+ return Optional.ofNullable(entities)
+ .map(
+ list ->
+ list.stream()
+ .map(record -> new NameAndId(record.getName(), record.getId()))
+ .collect(Collectors.toList()))
+ .orElse(null);
+ }
+
+ public PolarisEntity(@NotNull PolarisBaseEntity sourceEntity) {
+ super(
+ sourceEntity.getCatalogId(),
+ sourceEntity.getId(),
+ sourceEntity.getTypeCode(),
+ sourceEntity.getSubTypeCode(),
+ sourceEntity.getParentId(),
+ sourceEntity.getName());
+ this.createTimestamp = sourceEntity.getCreateTimestamp();
+ this.dropTimestamp = sourceEntity.getDropTimestamp();
+ this.purgeTimestamp = sourceEntity.getPurgeTimestamp();
+ this.lastUpdateTimestamp = sourceEntity.getLastUpdateTimestamp();
+ this.properties = sourceEntity.getProperties();
+ this.internalProperties = sourceEntity.getInternalProperties();
+ this.entityVersion = sourceEntity.getEntityVersion();
+ this.grantRecordsVersion = sourceEntity.getGrantRecordsVersion();
+ }
+
+ @JsonIgnore
+ public PolarisEntityType getType() {
+ return PolarisEntityType.fromCode(getTypeCode());
+ }
+
+ @JsonIgnore
+ public PolarisEntitySubType getSubType() {
+ return PolarisEntitySubType.fromCode(getSubTypeCode());
+ }
+
+ @JsonIgnore
+ public NameAndId nameAndId() {
+ return new NameAndId(name, id);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("name=" + getName());
+ sb.append(";id=" + getId());
+ sb.append(";parentId=" + getParentId());
+ sb.append(";entityVersion=" + getEntityVersion());
+ sb.append(";type=" + getType());
+ sb.append(";subType=" + getSubType());
+ sb.append(";internalProperties=" + getInternalPropertiesAsMap());
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof PolarisEntity)) return false;
+ PolarisEntity that = (PolarisEntity) o;
+ return catalogId == that.catalogId
+ && id == that.id
+ && parentId == that.parentId
+ && createTimestamp == that.createTimestamp
+ && dropTimestamp == that.dropTimestamp
+ && purgeTimestamp == that.purgeTimestamp
+ && lastUpdateTimestamp == that.lastUpdateTimestamp
+ && entityVersion == that.entityVersion
+ && grantRecordsVersion == that.grantRecordsVersion
+ && typeCode == that.typeCode
+ && subTypeCode == that.subTypeCode
+ && Objects.equals(name, that.name)
+ && Objects.equals(properties, that.properties)
+ && Objects.equals(internalProperties, that.internalProperties);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ typeCode,
+ subTypeCode,
+ catalogId,
+ id,
+ parentId,
+ name,
+ createTimestamp,
+ dropTimestamp,
+ purgeTimestamp,
+ lastUpdateTimestamp,
+ properties,
+ internalProperties,
+ entityVersion,
+ grantRecordsVersion);
+ }
+
+ public static class Builder extends BaseBuilder {
+ public Builder() {
+ super();
+ }
+
+ public Builder(PolarisEntity original) {
+ super(original);
+ }
+
+ public PolarisEntity build() {
+ return buildBase();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public abstract static class BaseBuilder> {
+ protected long catalogId;
+ protected PolarisEntityType type;
+ protected PolarisEntitySubType subType;
+ protected long id;
+ protected long parentId;
+ protected String name;
+ protected long createTimestamp;
+ protected long dropTimestamp;
+ protected long purgeTimestamp;
+ protected long lastUpdateTimestamp;
+ protected Map properties;
+ protected Map internalProperties;
+ protected int entityVersion;
+ protected int grantRecordsVersion;
+
+ protected BaseBuilder() {
+ this.catalogId = -1;
+ this.type = PolarisEntityType.NULL_TYPE;
+ this.subType = PolarisEntitySubType.NULL_SUBTYPE;
+ this.id = -1;
+ this.parentId = 0;
+ this.name = null;
+ this.createTimestamp = 0;
+ this.dropTimestamp = 0;
+ this.purgeTimestamp = 0;
+ this.lastUpdateTimestamp = 0;
+ this.properties = new HashMap<>();
+ this.internalProperties = new HashMap<>();
+ this.entityVersion = 1;
+ this.grantRecordsVersion = 1;
+ }
+
+ protected BaseBuilder(T original) {
+ this.catalogId = original.catalogId;
+ this.type = original.getType();
+ this.subType = original.getSubType();
+ this.id = original.id;
+ this.parentId = original.parentId;
+ this.name = original.name;
+ this.createTimestamp = original.createTimestamp;
+ this.dropTimestamp = original.dropTimestamp;
+ this.purgeTimestamp = original.purgeTimestamp;
+ this.lastUpdateTimestamp = original.lastUpdateTimestamp;
+ this.properties = new HashMap<>(original.getPropertiesAsMap());
+ this.internalProperties = new HashMap<>(original.getInternalPropertiesAsMap());
+ this.entityVersion = original.entityVersion;
+ this.grantRecordsVersion = original.grantRecordsVersion;
+ }
+
+ public abstract T build();
+
+ public PolarisEntity buildBase() {
+ // TODO: Validate required fields
+ // id > 0 already -- client must always supply id for idempotency purposes.
+ return new PolarisEntity(
+ catalogId,
+ type,
+ subType,
+ id,
+ parentId,
+ name,
+ createTimestamp,
+ dropTimestamp,
+ purgeTimestamp,
+ lastUpdateTimestamp,
+ properties,
+ internalProperties,
+ entityVersion,
+ grantRecordsVersion);
+ }
+
+ public B setCatalogId(long catalogId) {
+ this.catalogId = catalogId;
+ return (B) this;
+ }
+
+ public B setType(PolarisEntityType type) {
+ this.type = type;
+ return (B) this;
+ }
+
+ public B setSubType(PolarisEntitySubType subType) {
+ this.subType = subType;
+ return (B) this;
+ }
+
+ public B setId(long id) {
+ // TODO: Maybe block this one whenever builder is created from previously-existing entity
+ // since re-opening an entity should only be for modifying the mutable fields for a given
+ // logical entity. Would require separate builder type for "clone"-style copies, but
+ // usually when creating from other entity we want to preserve the id.
+ this.id = id;
+ return (B) this;
+ }
+
+ public B setParentId(long parentId) {
+ this.parentId = parentId;
+ return (B) this;
+ }
+
+ public B setName(String name) {
+ this.name = name;
+ return (B) this;
+ }
+
+ public B setCreateTimestamp(long createTimestamp) {
+ this.createTimestamp = createTimestamp;
+ return (B) this;
+ }
+
+ public B setDropTimestamp(long dropTimestamp) {
+ this.dropTimestamp = dropTimestamp;
+ return (B) this;
+ }
+
+ public B setPurgeTimestamp(long purgeTimestamp) {
+ this.purgeTimestamp = purgeTimestamp;
+ return (B) this;
+ }
+
+ public B setLastUpdateTimestamp(long lastUpdateTimestamp) {
+ this.lastUpdateTimestamp = lastUpdateTimestamp;
+ return (B) this;
+ }
+
+ public B setProperties(Map properties) {
+ this.properties = new HashMap<>(properties);
+ return (B) this;
+ }
+
+ public B addProperty(String key, String value) {
+ this.properties.put(key, value);
+ return (B) this;
+ }
+
+ public B setInternalProperties(Map internalProperties) {
+ this.internalProperties = new HashMap<>(internalProperties);
+ return (B) this;
+ }
+
+ public B setEntityVersion(int entityVersion) {
+ this.entityVersion = entityVersion;
+ return (B) this;
+ }
+
+ public B setGrantRecordsVersion(int grantRecordsVersion) {
+ this.grantRecordsVersion = grantRecordsVersion;
+ return (B) this;
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityActiveRecord.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityActiveRecord.java
new file mode 100644
index 0000000000..10461d6501
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityActiveRecord.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Objects;
+
+public class PolarisEntityActiveRecord {
+ // entity catalog id
+ private final long catalogId;
+
+ // id of the entity
+ private final long id;
+
+ // parent id of the entity
+ private final long parentId;
+
+ // name of the entity
+ private final String name;
+
+ // code representing the type of that entity
+ private final int typeCode;
+
+ // code representing the subtype of that entity
+ private final int subTypeCode;
+
+ public long getCatalogId() {
+ return catalogId;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public long getParentId() {
+ return parentId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getTypeCode() {
+ return typeCode;
+ }
+
+ public PolarisEntityType getType() {
+ return PolarisEntityType.fromCode(this.typeCode);
+ }
+
+ public int getSubTypeCode() {
+ return subTypeCode;
+ }
+
+ public PolarisEntitySubType getSubType() {
+ return PolarisEntitySubType.fromCode(this.subTypeCode);
+ }
+
+ @JsonCreator
+ public PolarisEntityActiveRecord(
+ @JsonProperty("catalogId") long catalogId,
+ @JsonProperty("id") long id,
+ @JsonProperty("parentId") long parentId,
+ @JsonProperty("name") String name,
+ @JsonProperty("typeCode") int typeCode,
+ @JsonProperty("subTypeCode") int subTypeCode) {
+ this.catalogId = catalogId;
+ this.id = id;
+ this.parentId = parentId;
+ this.name = name;
+ this.typeCode = typeCode;
+ this.subTypeCode = subTypeCode;
+ }
+
+ /** Constructor to create the object with provided entity */
+ public PolarisEntityActiveRecord(PolarisBaseEntity entity) {
+ this.catalogId = entity.getCatalogId();
+ this.id = entity.getId();
+ this.parentId = entity.getParentId();
+ this.typeCode = entity.getTypeCode();
+ this.name = entity.getName();
+ this.subTypeCode = entity.getSubTypeCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof PolarisEntityActiveRecord)) return false;
+ PolarisEntityActiveRecord that = (PolarisEntityActiveRecord) o;
+ return catalogId == that.catalogId
+ && id == that.id
+ && parentId == that.parentId
+ && typeCode == that.typeCode
+ && subTypeCode == that.subTypeCode
+ && Objects.equals(name, that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(catalogId, id, parentId, name, typeCode, subTypeCode);
+ }
+
+ @Override
+ public String toString() {
+ return "PolarisEntitiesActiveRecord{"
+ + "catalogId="
+ + catalogId
+ + ", id="
+ + id
+ + ", parentId="
+ + parentId
+ + ", name='"
+ + name
+ + '\''
+ + ", typeCode="
+ + typeCode
+ + ", subTypeCode="
+ + subTypeCode
+ + '}';
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java
new file mode 100644
index 0000000000..d3f562ddf4
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+public class PolarisEntityConstants {
+
+ public static final String ENTITY_BASE_LOCATION = "location";
+ // the key for the client_id property associated with a principal
+ private static final String CLIENT_ID_PROPERTY_NAME = "client_id";
+
+ // id of the root entity
+ private static final long ROOT_ENTITY_ID = 0L;
+
+ // special 0 value to represent a NULL value. For example the catalog id is null for a top-level
+ // entity like a catalog
+ private static final long NULL_ID = 0L;
+
+ // the name of the single root container representing an entire realm
+ private static final String ROOT_CONTAINER_NAME = "root_container";
+
+ // the name of the catalog/root admin role
+ private static final String ADMIN_CATALOG_ROLE_NAME = "catalog_admin";
+
+ // the name of the root principal we create at bootstrap time
+ private static final String ROOT_PRINCIPAL_NAME = "root";
+
+ // the name of the principal role we create to manage the entire Polaris service
+ private static final String ADMIN_PRINCIPAL_ROLE_NAME = "service_admin";
+
+ // 24 hours retention before purging. This should be a config
+ private static final long RETENTION_TIME_IN_MS = 24 * 3600_000;
+
+ private static final String STORAGE_CONFIGURATION_INFO_PROPERTY_NAME =
+ "storage_configuration_info";
+
+ private static final String STORAGE_INTEGRATION_IDENTIFIER_PROPERTY_NAME =
+ "storage_integration_identifier";
+
+ private static final String PRINCIPAL_TYPE_NAME = "principal_type_name";
+
+ public static final String PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE =
+ "CREDENTIAL_ROTATION_REQUIRED";
+
+ /**
+ * Name format of storage integration for polaris entity: POLARIS__ . This
+ * name format gives us flexibility to switch to use integration name in the future if we want.
+ */
+ public static final String POLARIS_STORAGE_INT_NAME_FORMAT = "POLARIS_%s_%s";
+
+ public static long getRootEntityId() {
+ return ROOT_ENTITY_ID;
+ }
+
+ public static long getNullId() {
+ return NULL_ID;
+ }
+
+ public static String getRootContainerName() {
+ return ROOT_CONTAINER_NAME;
+ }
+
+ public static String getNameOfCatalogAdminRole() {
+ return ADMIN_CATALOG_ROLE_NAME;
+ }
+
+ public static String getRootPrincipalName() {
+ return ROOT_PRINCIPAL_NAME;
+ }
+
+ public static String getNameOfPrincipalServiceAdminRole() {
+ return ADMIN_PRINCIPAL_ROLE_NAME;
+ }
+
+ public static long getRetentionTimeInMs() {
+ return RETENTION_TIME_IN_MS;
+ }
+
+ public static String getClientIdPropertyName() {
+ return CLIENT_ID_PROPERTY_NAME;
+ }
+
+ public static String getStorageIntegrationIdentifierPropertyName() {
+ return STORAGE_INTEGRATION_IDENTIFIER_PROPERTY_NAME;
+ }
+
+ public static String getStorageConfigInfoPropertyName() {
+ return STORAGE_CONFIGURATION_INFO_PROPERTY_NAME;
+ }
+
+ public static String getPolarisStorageIntegrationNameFormat() {
+ return POLARIS_STORAGE_INT_NAME_FORMAT;
+ }
+
+ public static String getPrincipalTypeName() {
+ return PRINCIPAL_TYPE_NAME;
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityCore.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityCore.java
new file mode 100644
index 0000000000..f084f42f58
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityCore.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import java.util.Objects;
+
+/**
+ * Core attributes representing basic information about an entity. Change generally means that the
+ * entity will be renamed, dropped, re-created, re-parented. Basically any change to the structure
+ * of the entity tree. For some operations like updating the entity, change will mean any change,
+ * i.e. entity version mismatch.
+ */
+public class PolarisEntityCore {
+
+ // the id of the catalog associated to that entity. NULL_ID if this entity is top-level like
+ // a catalog
+ protected long catalogId;
+
+ // the id of the entity which was resolved
+ protected long id;
+
+ // the id of the parent of this entity, use 0 for a top-level entity whose parent is the account
+ protected long parentId;
+
+ // the type of the entity when it was resolved
+ protected int typeCode;
+
+ // the name that this entity had when it was resolved
+ protected String name;
+
+ // the version that this entity had when it was resolved
+ protected int entityVersion;
+
+ public PolarisEntityCore() {}
+
+ public PolarisEntityCore(
+ long catalogId, long id, long parentId, int typeCode, String name, int entityVersion) {
+ this.catalogId = catalogId;
+ this.id = id;
+ this.parentId = parentId;
+ this.typeCode = typeCode;
+ this.name = name;
+ this.entityVersion = entityVersion;
+ }
+
+ public PolarisEntityCore(PolarisBaseEntity entity) {
+ this.catalogId = entity.getCatalogId();
+ this.id = entity.getId();
+ this.parentId = entity.getParentId();
+ this.typeCode = entity.getTypeCode();
+ this.name = entity.getName();
+ this.entityVersion = entity.getEntityVersion();
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public long getParentId() {
+ return parentId;
+ }
+
+ public void setParentId(long parentId) {
+ this.parentId = parentId;
+ }
+
+ public int getTypeCode() {
+ return typeCode;
+ }
+
+ public void setTypeCode(int typeCode) {
+ this.typeCode = typeCode;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getEntityVersion() {
+ return entityVersion;
+ }
+
+ public long getCatalogId() {
+ return catalogId;
+ }
+
+ public void setCatalogId(long catalogId) {
+ this.catalogId = catalogId;
+ }
+
+ public void setEntityVersion(int entityVersion) {
+ this.entityVersion = entityVersion;
+ }
+
+ /**
+ * @return the type of this entity
+ */
+ @JsonIgnore
+ public PolarisEntityType getType() {
+ return PolarisEntityType.fromCode(this.typeCode);
+ }
+
+ /**
+ * @return true if this entity cannot be dropped or renamed. Applies to the admin catalog role and
+ * the polaris service admin principal role.
+ */
+ @JsonIgnore
+ public boolean cannotBeDroppedOrRenamed() {
+ return (this.typeCode == PolarisEntityType.CATALOG_ROLE.getCode()
+ && this.name.equals(PolarisEntityConstants.getNameOfCatalogAdminRole()))
+ || (this.typeCode == PolarisEntityType.PRINCIPAL_ROLE.getCode()
+ && this.name.equals(PolarisEntityConstants.getNameOfPrincipalServiceAdminRole()));
+ }
+
+ /**
+ * @return true if this entity is top-level, like a catalog, a principal,
+ */
+ @JsonIgnore
+ public boolean isTopLevel() {
+ return this.getType().isTopLevel();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PolarisEntityCore)) {
+ return false;
+ }
+ PolarisEntityCore that = (PolarisEntityCore) o;
+ return catalogId == that.catalogId
+ && id == that.id
+ && parentId == that.parentId
+ && typeCode == that.typeCode
+ && entityVersion == that.entityVersion
+ && Objects.equals(name, that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(catalogId, id, parentId, typeCode, name, entityVersion);
+ }
+
+ @Override
+ public String toString() {
+ return "PolarisEntityCore{"
+ + "catalogId="
+ + catalogId
+ + ", id="
+ + id
+ + ", parentId="
+ + parentId
+ + ", typeCode="
+ + typeCode
+ + ", name='"
+ + name
+ + '\''
+ + ", entityVersion="
+ + entityVersion
+ + '}';
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityId.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityId.java
new file mode 100644
index 0000000000..74e2b57b21
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityId.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Objects;
+
+/** Simple record like class to represent the unique identifier of an entity */
+public class PolarisEntityId {
+
+ // id of the catalog for this entity. If this entity is top-level, this will be NULL. Only not
+ // null if this entity is a catalog entity like a namespace, a role, a table, a view, ...
+ private final long catalogId;
+
+ // entity id
+ private final long id;
+
+ @JsonCreator
+ public PolarisEntityId(@JsonProperty("catalogId") long catalogId, @JsonProperty("id") long id) {
+ this.catalogId = catalogId;
+ this.id = id;
+ }
+
+ public long getCatalogId() {
+ return catalogId;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PolarisEntityId that = (PolarisEntityId) o;
+ return catalogId == that.catalogId && id == that.id;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(catalogId, id);
+ }
+
+ @Override
+ public String toString() {
+ return "PolarisEntityId{" + "catalogId=" + catalogId + ", id=" + id + '}';
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitySubType.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitySubType.java
new file mode 100644
index 0000000000..c36b75bdc0
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitySubType.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import org.jetbrains.annotations.Nullable;
+
+/** Subtype for an entity */
+public enum PolarisEntitySubType {
+ // ANY_SUBTYPE is not stored but is used to indicate that any subtype entities should be
+ // returned, for example when doing a list operation or checking if a table like object of
+ // name X exists
+ ANY_SUBTYPE(-1, null),
+ // the NULL value is used when an entity has no subtype, i.e. NOT_APPLICABLE really
+ NULL_SUBTYPE(0, null),
+ TABLE(2, PolarisEntityType.TABLE_LIKE),
+ VIEW(3, PolarisEntityType.TABLE_LIKE);
+
+ // to efficiently map the code of a subtype to its corresponding subtype enum, use a reverse
+ // array which is initialized below
+ private static final PolarisEntitySubType[] REVERSE_MAPPING_ARRAY;
+
+ static {
+ // find max array size
+ int maxId = 0;
+ for (PolarisEntitySubType entitySubType : PolarisEntitySubType.values()) {
+ if (maxId < entitySubType.code) {
+ maxId = entitySubType.code;
+ }
+ }
+
+ // allocate mapping array
+ REVERSE_MAPPING_ARRAY = new PolarisEntitySubType[maxId + 1];
+
+ // populate mapping array, only for positive indices
+ for (PolarisEntitySubType entitySubType : PolarisEntitySubType.values()) {
+ if (entitySubType.code >= 0) {
+ REVERSE_MAPPING_ARRAY[entitySubType.code] = entitySubType;
+ }
+ }
+ }
+
+ // unique code associated to that entity subtype
+ private final int code;
+
+ // parent type for this entity
+ private final PolarisEntityType parentType;
+
+ PolarisEntitySubType(int code, PolarisEntityType parentType) {
+ // remember the id of this entity
+ this.code = code;
+ this.parentType = parentType;
+ }
+
+ /**
+ * @return the code associated to a subtype, will be stored in FDB
+ */
+ @JsonValue
+ public int getCode() {
+ return code;
+ }
+
+ /**
+ * @return parent type of that entity
+ */
+ public PolarisEntityType getParentType() {
+ return this.parentType;
+ }
+
+ /**
+ * Given the id of the subtype of an entity, return the subtype associated to it. Return null if
+ * not found
+ *
+ * @param entitySubTypeCode code associated to the entity type
+ * @return entity subtype corresponding to that code or null if mapping not found
+ */
+ @JsonCreator
+ public static @Nullable PolarisEntitySubType fromCode(int entitySubTypeCode) {
+ // ensure it is within bounds
+ if (entitySubTypeCode >= REVERSE_MAPPING_ARRAY.length) {
+ return null;
+ }
+
+ // get value
+ if (entitySubTypeCode >= 0) {
+ return REVERSE_MAPPING_ARRAY[entitySubTypeCode];
+ } else {
+ for (PolarisEntitySubType entitySubType : PolarisEntitySubType.values()) {
+ if (entitySubType.code == entitySubTypeCode) {
+ return entitySubType;
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityType.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityType.java
new file mode 100644
index 0000000000..f920efbfd1
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityType.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import org.jetbrains.annotations.Nullable;
+
+/** Types of entities with their id */
+public enum PolarisEntityType {
+ NULL_TYPE(0, null, false, false),
+ ROOT(1, null, false, false),
+ PRINCIPAL(2, ROOT, true, false),
+ PRINCIPAL_ROLE(3, ROOT, true, false),
+ CATALOG(4, ROOT, false, false),
+ CATALOG_ROLE(5, CATALOG, true, false),
+ NAMESPACE(6, CATALOG, false, true),
+ // generic table is either a view or a real table
+ TABLE_LIKE(7, NAMESPACE, false, false),
+ TASK(8, ROOT, false, false),
+ FILE(9, TABLE_LIKE, false, false);
+
+ // to efficiently map a code to its corresponding entity type, use a reverse array which
+ // is initialized below
+ private static final PolarisEntityType[] REVERSE_MAPPING_ARRAY;
+
+ static {
+ // find max array size
+ int maxId = 0;
+ for (PolarisEntityType entityType : PolarisEntityType.values()) {
+ if (maxId < entityType.code) {
+ maxId = entityType.code;
+ }
+ }
+
+ // allocate mapping array
+ REVERSE_MAPPING_ARRAY = new PolarisEntityType[maxId + 1];
+
+ // populate mapping array
+ for (PolarisEntityType entityType : PolarisEntityType.values()) {
+ REVERSE_MAPPING_ARRAY[entityType.code] = entityType;
+ }
+ }
+
+ // unique id for an entity type
+ private final int code;
+
+ // true if this entity is a grantee, i.e. is an entity which can be on the receiving end of
+ // a grant. Only roles and principals are grantees
+ private final boolean isGrantee;
+
+ // true if the parent entity type can also be the same type (e.g. namespaces)
+ private final boolean parentSelfReference;
+
+ // parent entity type, null for an ACCOUNT
+ private final PolarisEntityType parentType;
+
+ PolarisEntityType(int id, PolarisEntityType parentType, boolean isGrantee, boolean sefRef) {
+ // remember the id of this entity
+ this.code = id;
+ this.isGrantee = isGrantee;
+ this.parentType = parentType;
+ this.parentSelfReference = sefRef;
+ }
+
+ /**
+ * @return the code associated to the specified the entity type, will be stored in FDB
+ */
+ @JsonValue
+ public int getCode() {
+ return code;
+ }
+
+ /**
+ * @return true if this entity is a grantee, i.e. an entity which can receive grants
+ */
+ public boolean isGrantee() {
+ return this.isGrantee;
+ }
+
+ /**
+ * @return true if this entity can be nested with itself (like a NAMESPACE)
+ */
+ public boolean isParentSelfReference() {
+ return parentSelfReference;
+ }
+
+ /**
+ * Given the code associated to the type of entity, return the subtype associated to it. Return
+ * null if not found
+ *
+ * @param entityTypeCode code associated to the entity type
+ * @return entity type corresponding to that code or null if mapping not found
+ */
+ @JsonCreator
+ public static @Nullable PolarisEntityType fromCode(int entityTypeCode) {
+ // ensure it is within bounds
+ if (entityTypeCode >= REVERSE_MAPPING_ARRAY.length) {
+ return null;
+ }
+
+ // get value
+ return REVERSE_MAPPING_ARRAY[entityTypeCode];
+ }
+
+ /**
+ * @return TRUE if this entity is top-level
+ */
+ public boolean isTopLevel() {
+ return (this.parentType == ROOT || this == ROOT);
+ }
+
+ /**
+ * @return the parent type of this type in the entity hierarchy
+ */
+ public PolarisEntityType getParentType() {
+ return this.parentType;
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisGrantRecord.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisGrantRecord.java
new file mode 100644
index 0000000000..7af2a2ee38
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisGrantRecord.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PolarisGrantRecord {
+
+ // id of the catalog where the securable entity resides, NULL_ID if this entity is a top-level
+ // account entity
+ private long securableCatalogId;
+
+ // id of the securable
+ private long securableId;
+
+ // id of the catalog where the grantee entity resides, NULL_ID if this entity is a top-level
+ // account entity
+ private long granteeCatalogId;
+
+ // id of the grantee
+ private long granteeId;
+
+ // id associated to the privilege
+ private int privilegeCode;
+
+ public PolarisGrantRecord() {}
+
+ public long getSecurableCatalogId() {
+ return securableCatalogId;
+ }
+
+ public void setSecurableCatalogId(long securableCatalogId) {
+ this.securableCatalogId = securableCatalogId;
+ }
+
+ public long getSecurableId() {
+ return securableId;
+ }
+
+ public void setSecurableId(long securableId) {
+ this.securableId = securableId;
+ }
+
+ public long getGranteeCatalogId() {
+ return granteeCatalogId;
+ }
+
+ public void setGranteeCatalogId(long granteeCatalogId) {
+ this.granteeCatalogId = granteeCatalogId;
+ }
+
+ public long getGranteeId() {
+ return granteeId;
+ }
+
+ public void setGranteeId(long granteeId) {
+ this.granteeId = granteeId;
+ }
+
+ public int getPrivilegeCode() {
+ return privilegeCode;
+ }
+
+ public void setPrivilegeCode(int privilegeCode) {
+ this.privilegeCode = privilegeCode;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param securableCatalogId catalog id for the securable. Can be NULL_ID if securable is
+ * top-level account entity
+ * @param securableId id of the securable
+ * @param granteeCatalogId catalog id for the grantee, Can be NULL_ID if grantee is top-level
+ * account entity
+ * @param granteeId id of the grantee
+ * @param privilegeCode privilege being granted to the grantee on the securable
+ */
+ @JsonCreator
+ public PolarisGrantRecord(
+ @JsonProperty("securableCatalogId") long securableCatalogId,
+ @JsonProperty("securableId") long securableId,
+ @JsonProperty("granteeCatalogId") long granteeCatalogId,
+ @JsonProperty("granteeId") long granteeId,
+ @JsonProperty("privilegeCode") int privilegeCode) {
+ this.securableCatalogId = securableCatalogId;
+ this.securableId = securableId;
+ this.granteeCatalogId = granteeCatalogId;
+ this.granteeId = granteeId;
+ this.privilegeCode = privilegeCode;
+ }
+
+ /**
+ * Copy constructor
+ *
+ * @param grantRec grant rec to copy
+ */
+ public PolarisGrantRecord(PolarisGrantRecord grantRec) {
+ this.securableCatalogId = grantRec.getSecurableCatalogId();
+ this.securableId = grantRec.getSecurableId();
+ this.granteeCatalogId = grantRec.getGranteeCatalogId();
+ this.granteeId = grantRec.getGranteeId();
+ this.privilegeCode = grantRec.getPrivilegeCode();
+ }
+
+ @Override
+ public String toString() {
+ return "PolarisGrantRec{"
+ + "securableCatalogId="
+ + securableCatalogId
+ + ", securableId="
+ + securableId
+ + ", granteeCatalogId="
+ + granteeCatalogId
+ + ", granteeId="
+ + granteeId
+ + ", privilegeCode="
+ + privilegeCode
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PolarisGrantRecord that = (PolarisGrantRecord) o;
+ return securableCatalogId == that.securableCatalogId
+ && securableId == that.securableId
+ && granteeCatalogId == that.granteeCatalogId
+ && granteeId == that.granteeId
+ && privilegeCode == that.privilegeCode;
+ }
+
+ @Override
+ public int hashCode() {
+ return java.util.Objects.hash(
+ securableCatalogId, securableId, granteeCatalogId, granteeId, privilegeCode);
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrincipalSecrets.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrincipalSecrets.java
new file mode 100644
index 0000000000..7efab8f530
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrincipalSecrets.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.security.SecureRandom;
+
+/**
+ * Simple class to represent the secrets used to authenticate a catalog principal, These secrets are
+ * managed separately.
+ */
+public class PolarisPrincipalSecrets {
+
+ // secure random number generator
+ private static final SecureRandom secureRandom = new SecureRandom();
+
+ // the id of the principal
+ private final long principalId;
+
+ // the client id for that principal
+ private final String principalClientId;
+
+ // the main secret for that principal
+ private String mainSecret;
+
+ // the secondary secret for that principal
+ private String secondarySecret;
+
+ /**
+ * Generate a secure random string
+ *
+ * @return the secure random string we generated
+ */
+ private String generateRandomHexString(int stringLength) {
+
+ // generate random byte array
+ byte[] randomBytes =
+ new byte[stringLength / 2]; // Each byte will be represented by two hex characters
+ secureRandom.nextBytes(randomBytes);
+
+ // build string
+ StringBuilder sb = new StringBuilder();
+ for (byte randomByte : randomBytes) {
+ sb.append(String.format("%02x", randomByte));
+ }
+
+ return sb.toString();
+ }
+
+ @JsonCreator
+ public PolarisPrincipalSecrets(
+ @JsonProperty("principalId") long principalId,
+ @JsonProperty("principalClientId") String principalClientId,
+ @JsonProperty("mainSecret") String mainSecret,
+ @JsonProperty("secondarySecret") String secondarySecret) {
+ this.principalId = principalId;
+ this.principalClientId = principalClientId;
+ this.mainSecret = mainSecret;
+ this.secondarySecret = secondarySecret;
+ }
+
+ public PolarisPrincipalSecrets(PolarisPrincipalSecrets principalSecrets) {
+ this.principalId = principalSecrets.getPrincipalId();
+ this.principalClientId = principalSecrets.getPrincipalClientId();
+ this.mainSecret = principalSecrets.getMainSecret();
+ this.secondarySecret = principalSecrets.getSecondarySecret();
+ }
+
+ public PolarisPrincipalSecrets(long principalId) {
+ this.principalId = principalId;
+ this.principalClientId = this.generateRandomHexString(16);
+ this.mainSecret = this.generateRandomHexString(32);
+ this.secondarySecret = this.generateRandomHexString(32);
+ }
+
+ /**
+ * Rotate the main secrets
+ *
+ * @param mainSecretToRotate the main secrets to rotate
+ */
+ public void rotateSecrets(String mainSecretToRotate) {
+ this.secondarySecret = mainSecretToRotate;
+ this.mainSecret = this.generateRandomHexString(32);
+ }
+
+ public long getPrincipalId() {
+ return principalId;
+ }
+
+ public String getPrincipalClientId() {
+ return principalClientId;
+ }
+
+ public String getMainSecret() {
+ return mainSecret;
+ }
+
+ public String getSecondarySecret() {
+ return secondarySecret;
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrivilege.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrivilege.java
new file mode 100644
index 0000000000..3215825d76
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrivilege.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/** List of privileges */
+public enum PolarisPrivilege {
+ SERVICE_MANAGE_ACCESS(1, PolarisEntityType.ROOT),
+ CATALOG_MANAGE_ACCESS(2, PolarisEntityType.CATALOG),
+ CATALOG_ROLE_USAGE(
+ 3,
+ PolarisEntityType.CATALOG_ROLE,
+ PolarisEntitySubType.NULL_SUBTYPE,
+ PolarisEntityType.PRINCIPAL_ROLE),
+ PRINCIPAL_ROLE_USAGE(
+ 4,
+ PolarisEntityType.PRINCIPAL_ROLE,
+ PolarisEntitySubType.NULL_SUBTYPE,
+ PolarisEntityType.PRINCIPAL),
+ NAMESPACE_CREATE(5, PolarisEntityType.NAMESPACE),
+ TABLE_CREATE(6, PolarisEntityType.NAMESPACE),
+ VIEW_CREATE(7, PolarisEntityType.NAMESPACE),
+ NAMESPACE_DROP(8, PolarisEntityType.NAMESPACE),
+ TABLE_DROP(9, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE),
+ VIEW_DROP(10, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW),
+ NAMESPACE_LIST(11, PolarisEntityType.NAMESPACE),
+ TABLE_LIST(12, PolarisEntityType.NAMESPACE),
+ VIEW_LIST(13, PolarisEntityType.NAMESPACE),
+ NAMESPACE_READ_PROPERTIES(14, PolarisEntityType.NAMESPACE),
+ TABLE_READ_PROPERTIES(15, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE),
+ VIEW_READ_PROPERTIES(16, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW),
+ NAMESPACE_WRITE_PROPERTIES(17, PolarisEntityType.NAMESPACE),
+ TABLE_WRITE_PROPERTIES(18, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE),
+ VIEW_WRITE_PROPERTIES(19, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW),
+ TABLE_READ_DATA(20, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE),
+ TABLE_WRITE_DATA(21, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE),
+ NAMESPACE_FULL_METADATA(22, PolarisEntityType.NAMESPACE),
+ TABLE_FULL_METADATA(23, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE),
+ VIEW_FULL_METADATA(24, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW),
+ CATALOG_CREATE(25, PolarisEntityType.ROOT),
+ CATALOG_DROP(26, PolarisEntityType.CATALOG),
+ CATALOG_LIST(27, PolarisEntityType.ROOT),
+ CATALOG_READ_PROPERTIES(28, PolarisEntityType.CATALOG),
+ CATALOG_WRITE_PROPERTIES(29, PolarisEntityType.CATALOG),
+ CATALOG_FULL_METADATA(30, PolarisEntityType.CATALOG),
+ CATALOG_MANAGE_METADATA(31, PolarisEntityType.CATALOG),
+ CATALOG_MANAGE_CONTENT(32, PolarisEntityType.CATALOG),
+ PRINCIPAL_LIST_GRANTS(33, PolarisEntityType.PRINCIPAL),
+ PRINCIPAL_ROLE_LIST_GRANTS(34, PolarisEntityType.PRINCIPAL),
+ CATALOG_ROLE_LIST_GRANTS(35, PolarisEntityType.PRINCIPAL),
+ CATALOG_LIST_GRANTS(36, PolarisEntityType.CATALOG),
+ NAMESPACE_LIST_GRANTS(37, PolarisEntityType.NAMESPACE),
+ TABLE_LIST_GRANTS(38, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE),
+ VIEW_LIST_GRANTS(39, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW),
+ CATALOG_MANAGE_GRANTS_ON_SECURABLE(40, PolarisEntityType.CATALOG),
+ NAMESPACE_MANAGE_GRANTS_ON_SECURABLE(41, PolarisEntityType.NAMESPACE),
+ TABLE_MANAGE_GRANTS_ON_SECURABLE(42, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE),
+ VIEW_MANAGE_GRANTS_ON_SECURABLE(43, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW),
+ PRINCIPAL_CREATE(44, PolarisEntityType.ROOT),
+ PRINCIPAL_DROP(45, PolarisEntityType.PRINCIPAL),
+ PRINCIPAL_LIST(46, PolarisEntityType.ROOT),
+ PRINCIPAL_READ_PROPERTIES(47, PolarisEntityType.PRINCIPAL),
+ PRINCIPAL_WRITE_PROPERTIES(48, PolarisEntityType.PRINCIPAL),
+ PRINCIPAL_FULL_METADATA(49, PolarisEntityType.PRINCIPAL),
+ PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE(50, PolarisEntityType.PRINCIPAL),
+ PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE(51, PolarisEntityType.PRINCIPAL),
+ PRINCIPAL_ROTATE_CREDENTIALS(52, PolarisEntityType.PRINCIPAL),
+ PRINCIPAL_RESET_CREDENTIALS(53, PolarisEntityType.PRINCIPAL),
+ PRINCIPAL_ROLE_CREATE(54, PolarisEntityType.ROOT),
+ PRINCIPAL_ROLE_DROP(55, PolarisEntityType.PRINCIPAL_ROLE),
+ PRINCIPAL_ROLE_LIST(56, PolarisEntityType.ROOT),
+ PRINCIPAL_ROLE_READ_PROPERTIES(57, PolarisEntityType.PRINCIPAL_ROLE),
+ PRINCIPAL_ROLE_WRITE_PROPERTIES(58, PolarisEntityType.PRINCIPAL_ROLE),
+ PRINCIPAL_ROLE_FULL_METADATA(59, PolarisEntityType.PRINCIPAL_ROLE),
+ PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE(60, PolarisEntityType.PRINCIPAL_ROLE),
+ PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE(61, PolarisEntityType.PRINCIPAL_ROLE),
+ CATALOG_ROLE_CREATE(62, PolarisEntityType.CATALOG),
+ CATALOG_ROLE_DROP(63, PolarisEntityType.CATALOG_ROLE),
+ CATALOG_ROLE_LIST(64, PolarisEntityType.CATALOG),
+ CATALOG_ROLE_READ_PROPERTIES(65, PolarisEntityType.CATALOG_ROLE),
+ CATALOG_ROLE_WRITE_PROPERTIES(66, PolarisEntityType.CATALOG_ROLE),
+ CATALOG_ROLE_FULL_METADATA(67, PolarisEntityType.CATALOG_ROLE),
+ CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE(68, PolarisEntityType.CATALOG_ROLE),
+ CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE(69, PolarisEntityType.CATALOG_ROLE),
+ ;
+
+ /**
+ * Full constructor
+ *
+ * @param code internal code associated to this privilege
+ * @param securableType securable type
+ * @param securableSubType securable subtype, mostly NULL_SUBTYPE
+ * @param granteeType grantee type, generally a ROLE
+ */
+ PolarisPrivilege(
+ int code,
+ @NotNull PolarisEntityType securableType,
+ @NotNull PolarisEntitySubType securableSubType,
+ @NotNull PolarisEntityType granteeType) {
+ this.code = code;
+ this.securableType = securableType;
+ this.securableSubType = securableSubType;
+ this.granteeType = granteeType;
+ }
+
+ /**
+ * Simple constructor, when the grantee is a role and the securable subtype is NULL_SUBTYPE
+ *
+ * @param code internal code associated to this privilege
+ * @param securableType securable type
+ */
+ PolarisPrivilege(int code, @NotNull PolarisEntityType securableType) {
+ this.code = code;
+ this.securableType = securableType;
+ this.securableSubType = PolarisEntitySubType.NULL_SUBTYPE;
+ this.granteeType = PolarisEntityType.CATALOG_ROLE;
+ }
+
+ /**
+ * Constructor when the grantee is a ROLE
+ *
+ * @param code internal code associated to this privilege
+ * @param securableType securable type
+ * @param securableSubType securable subtype, mostly NULL_SUBTYPE
+ */
+ PolarisPrivilege(
+ int code,
+ @NotNull PolarisEntityType securableType,
+ @NotNull PolarisEntitySubType securableSubType) {
+ this.code = code;
+ this.securableType = securableType;
+ this.securableSubType = securableSubType;
+ this.granteeType = PolarisEntityType.CATALOG_ROLE;
+ }
+
+ // internal code used to represent this privilege
+ private final int code;
+
+ // the type of the securable for this privilege
+ private final PolarisEntityType securableType;
+
+ // the subtype of the securable for this privilege
+ private final PolarisEntitySubType securableSubType;
+
+ // the type of the securable for this privilege
+ private final PolarisEntityType granteeType;
+
+ // to efficiently map a code to its corresponding entity type, use a reverse array which
+ // is initialized below
+ private static final PolarisPrivilege[] REVERSE_MAPPING_ARRAY;
+
+ static {
+ // find max array size
+ int maxId = 0;
+ for (PolarisPrivilege privilegeDef : PolarisPrivilege.values()) {
+ if (maxId < privilegeDef.code) {
+ maxId = privilegeDef.code;
+ }
+ }
+
+ // allocate mapping array
+ REVERSE_MAPPING_ARRAY = new PolarisPrivilege[maxId + 1];
+
+ // populate mapping array
+ for (PolarisPrivilege privilegeDef : PolarisPrivilege.values()) {
+ REVERSE_MAPPING_ARRAY[privilegeDef.code] = privilegeDef;
+ }
+ }
+
+ /**
+ * @return the code associated to the specified privilege
+ */
+ @JsonValue
+ public int getCode() {
+ return code;
+ }
+
+ /**
+ * Given the code associated to a privilege, return the privilege associated to it. Return null if
+ * not found
+ *
+ * @param code code associated to the entity type
+ * @return entity type corresponding to that code or null if mapping not found
+ */
+ @JsonCreator
+ public static @Nullable PolarisPrivilege fromCode(int code) {
+ // ensure it is within bounds
+ if (code >= REVERSE_MAPPING_ARRAY.length) {
+ return null;
+ }
+
+ // get value
+ return REVERSE_MAPPING_ARRAY[code];
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisTaskConstants.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisTaskConstants.java
new file mode 100644
index 0000000000..36961eb9f8
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisTaskConstants.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+/** Constants used to store task properties and configuration parameters */
+public class PolarisTaskConstants {
+ public static final long TASK_TIMEOUT_MILLIS = 300000;
+ public static final String TASK_TIMEOUT_MILLIS_CONFIG = "POLARIS_TASK_TIMEOUT_MILLIS";
+ public static final String LAST_ATTEMPT_EXECUTOR_ID = "lastAttemptExecutorId";
+ public static final String LAST_ATTEMPT_START_TIME = "lastAttemptStartTime";
+ public static final String ATTEMPT_COUNT = "attemptCount";
+ public static final String TASK_DATA = "data";
+ public static final String TASK_TYPE = "taskType";
+ public static final String STORAGE_LOCATION = "storageLocation";
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PrincipalEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalEntity.java
new file mode 100644
index 0000000000..eaa8bfc7e3
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalEntity.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import io.polaris.core.admin.model.Principal;
+
+/** Wrapper for translating between the REST Principal object and the base PolarisEntity type. */
+public class PrincipalEntity extends PolarisEntity {
+ public PrincipalEntity(PolarisBaseEntity sourceEntity) {
+ super(sourceEntity);
+ }
+
+ public static PrincipalEntity of(PolarisBaseEntity sourceEntity) {
+ if (sourceEntity != null) {
+ return new PrincipalEntity(sourceEntity);
+ }
+ return null;
+ }
+
+ public static PrincipalEntity fromPrincipal(Principal principal) {
+ return new Builder()
+ .setName(principal.getName())
+ .setProperties(principal.getProperties())
+ .setClientId(principal.getClientId())
+ .build();
+ }
+
+ public Principal asPrincipal() {
+ return new Principal(
+ getName(),
+ getClientId(),
+ getPropertiesAsMap(),
+ getCreateTimestamp(),
+ getLastUpdateTimestamp(),
+ getEntityVersion());
+ }
+
+ public String getClientId() {
+ return getInternalPropertiesAsMap().get(PolarisEntityConstants.getClientIdPropertyName());
+ }
+
+ public static class Builder extends PolarisEntity.BaseBuilder {
+ public Builder() {
+ super();
+ setType(PolarisEntityType.PRINCIPAL);
+ setCatalogId(PolarisEntityConstants.getNullId());
+ setParentId(PolarisEntityConstants.getRootEntityId());
+ }
+
+ public Builder(PrincipalEntity original) {
+ super(original);
+ }
+
+ public Builder setClientId(String clientId) {
+ internalProperties.put(PolarisEntityConstants.getClientIdPropertyName(), clientId);
+ return this;
+ }
+
+ public Builder setCredentialRotationRequiredState() {
+ internalProperties.put(
+ PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE, "true");
+ return this;
+ }
+
+ public PrincipalEntity build() {
+ return new PrincipalEntity(buildBase());
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PrincipalRoleEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalRoleEntity.java
new file mode 100644
index 0000000000..44732e875b
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalRoleEntity.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import io.polaris.core.admin.model.PrincipalRole;
+
+/**
+ * Wrapper for translating between the REST PrincipalRole object and the base PolarisEntity type.
+ */
+public class PrincipalRoleEntity extends PolarisEntity {
+ public PrincipalRoleEntity(PolarisBaseEntity sourceEntity) {
+ super(sourceEntity);
+ }
+
+ public static PrincipalRoleEntity of(PolarisBaseEntity sourceEntity) {
+ if (sourceEntity != null) {
+ return new PrincipalRoleEntity(sourceEntity);
+ }
+ return null;
+ }
+
+ public static PrincipalRoleEntity fromPrincipalRole(PrincipalRole principalRole) {
+ return new Builder()
+ .setName(principalRole.getName())
+ .setProperties(principalRole.getProperties())
+ .build();
+ }
+
+ public PrincipalRole asPrincipalRole() {
+ PrincipalRole principalRole =
+ new PrincipalRole(
+ getName(),
+ getPropertiesAsMap(),
+ getCreateTimestamp(),
+ getLastUpdateTimestamp(),
+ getEntityVersion());
+ return principalRole;
+ }
+
+ public static class Builder extends PolarisEntity.BaseBuilder {
+ public Builder() {
+ super();
+ setType(PolarisEntityType.PRINCIPAL_ROLE);
+ setCatalogId(PolarisEntityConstants.getNullId());
+ setParentId(PolarisEntityConstants.getRootEntityId());
+ }
+
+ public Builder(PrincipalRoleEntity original) {
+ super(original);
+ }
+
+ public PrincipalRoleEntity build() {
+ return new PrincipalRoleEntity(buildBase());
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java
new file mode 100644
index 0000000000..6aab5d2c6f
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.RESTUtil;
+
+public class TableLikeEntity extends PolarisEntity {
+ // For applicable types, this key on the "internalProperties" map will return the location
+ // of the internalProperties JSON file.
+ public static final String METADATA_LOCATION_KEY = "metadata-location";
+
+ public static final String USER_SPECIFIED_WRITE_DATA_LOCATION_KEY = "write.data.path";
+ public static final String USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY = "write.metadata.path";
+
+ public TableLikeEntity(PolarisBaseEntity sourceEntity) {
+ super(sourceEntity);
+ }
+
+ public static TableLikeEntity of(PolarisBaseEntity sourceEntity) {
+ if (sourceEntity != null) {
+ return new TableLikeEntity(sourceEntity);
+ }
+ return null;
+ }
+
+ @JsonIgnore
+ public TableIdentifier getTableIdentifier() {
+ Namespace parent = getParentNamespace();
+ return TableIdentifier.of(parent, getName());
+ }
+
+ @JsonIgnore
+ public Namespace getParentNamespace() {
+ String encodedNamespace =
+ getInternalPropertiesAsMap().get(NamespaceEntity.PARENT_NAMESPACE_KEY);
+ if (encodedNamespace == null) {
+ return Namespace.empty();
+ }
+ return RESTUtil.decodeNamespace(encodedNamespace);
+ }
+
+ @JsonIgnore
+ public String getMetadataLocation() {
+ return getInternalPropertiesAsMap().get(METADATA_LOCATION_KEY);
+ }
+
+ @JsonIgnore
+ public String getBaseLocation() {
+ return getPropertiesAsMap().get(PolarisEntityConstants.ENTITY_BASE_LOCATION);
+ }
+
+ public static class Builder extends PolarisEntity.BaseBuilder {
+ public Builder(TableIdentifier identifier, String metadataLocation) {
+ super();
+ setType(PolarisEntityType.TABLE_LIKE);
+ setTableIdentifier(identifier);
+ setMetadataLocation(metadataLocation);
+ }
+
+ public Builder(TableLikeEntity original) {
+ super(original);
+ }
+
+ public TableLikeEntity build() {
+ return new TableLikeEntity(buildBase());
+ }
+
+ public Builder setTableIdentifier(TableIdentifier identifier) {
+ Namespace namespace = identifier.namespace();
+ setParentNamespace(namespace);
+ setName(identifier.name());
+ return this;
+ }
+
+ public Builder setParentNamespace(Namespace namespace) {
+ if (namespace != null && !namespace.isEmpty()) {
+ internalProperties.put(
+ NamespaceEntity.PARENT_NAMESPACE_KEY, RESTUtil.encodeNamespace(namespace));
+ }
+ return this;
+ }
+
+ public Builder setBaseLocation(String location) {
+ properties.put(PolarisEntityConstants.ENTITY_BASE_LOCATION, location);
+ return this;
+ }
+
+ public Builder setMetadataLocation(String location) {
+ internalProperties.put(METADATA_LOCATION_KEY, location);
+ return this;
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/entity/TaskEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/TaskEntity.java
new file mode 100644
index 0000000000..ca2d7d17c2
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/entity/TaskEntity.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.entity;
+
+import io.polaris.core.PolarisCallContext;
+import io.polaris.core.context.CallContext;
+import io.polaris.core.persistence.PolarisObjectMapperUtil;
+
+/**
+ * Represents an asynchronous task entity in the persistence layer. A task executor is responsible
+ * for constructing the actual task instance based on the "data" and "taskType" properties
+ */
+public class TaskEntity extends PolarisEntity {
+ public TaskEntity(PolarisBaseEntity sourceEntity) {
+ super(sourceEntity);
+ }
+
+ public static TaskEntity of(PolarisBaseEntity polarisEntity) {
+ if (polarisEntity != null) {
+ return new TaskEntity(polarisEntity);
+ } else {
+ return null;
+ }
+ }
+
+ public T readData(Class klass) {
+ PolarisCallContext polarisCallContext = CallContext.getCurrentContext().getPolarisCallContext();
+ return PolarisObjectMapperUtil.deserialize(
+ polarisCallContext, getPropertiesAsMap().get(PolarisTaskConstants.TASK_DATA), klass);
+ }
+
+ public AsyncTaskType getTaskType() {
+ PolarisCallContext polarisCallContext = CallContext.getCurrentContext().getPolarisCallContext();
+ return PolarisObjectMapperUtil.deserialize(
+ polarisCallContext,
+ getPropertiesAsMap().get(PolarisTaskConstants.TASK_TYPE),
+ AsyncTaskType.class);
+ }
+
+ public static class Builder extends PolarisEntity.BaseBuilder {
+ public Builder() {
+ super();
+ setType(PolarisEntityType.TASK);
+ setCatalogId(PolarisEntityConstants.getNullId());
+ setParentId(PolarisEntityConstants.getRootEntityId());
+ }
+
+ public Builder(TaskEntity original) {
+ super(original);
+ }
+
+ public Builder withTaskType(AsyncTaskType taskType) {
+ PolarisCallContext polarisCallContext =
+ CallContext.getCurrentContext().getPolarisCallContext();
+ properties.put(
+ PolarisTaskConstants.TASK_TYPE,
+ PolarisObjectMapperUtil.serialize(polarisCallContext, taskType));
+ return this;
+ }
+
+ public Builder withData(Object data) {
+ PolarisCallContext polarisCallContext =
+ CallContext.getCurrentContext().getPolarisCallContext();
+ properties.put(
+ PolarisTaskConstants.TASK_DATA,
+ PolarisObjectMapperUtil.serialize(polarisCallContext, data));
+ return this;
+ }
+
+ public Builder withLastAttemptExecutorId(String executorId) {
+ properties.put(PolarisTaskConstants.LAST_ATTEMPT_EXECUTOR_ID, executorId);
+ return this;
+ }
+
+ public Builder withAttemptCount(int count) {
+ properties.put(PolarisTaskConstants.ATTEMPT_COUNT, String.valueOf(count));
+ return this;
+ }
+
+ public Builder withLastAttemptStartedTimestamp(long timestamp) {
+ properties.put(PolarisTaskConstants.LAST_ATTEMPT_START_TIME, String.valueOf(timestamp));
+ return this;
+ }
+
+ public TaskEntity build() {
+ return new TaskEntity(buildBase());
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java b/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java
new file mode 100644
index 0000000000..b3b8779f1d
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.monitor;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Timer;
+import io.polaris.core.resource.TimedApi;
+import java.lang.reflect.Method;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Wrapper around the Micrometer {@link MeterRegistry} providing additional metric management
+ * functions for the Polaris application. Implements in-memory caching of timers and counters.
+ * Records two metrics for each instrument with one tagged by the realm ID (realm-specific metric)
+ * and one without. The realm-specific metric is suffixed with ".realm".
+ */
+public class PolarisMetricRegistry {
+ private final MeterRegistry meterRegistry;
+ private final ConcurrentMap timers = new ConcurrentHashMap<>();
+ private final ConcurrentMap counters = new ConcurrentHashMap<>();
+ private static final String TAG_REALM = "REALM_ID";
+ private static final String TAG_RESP_CODE = "HTTP_RESPONSE_CODE";
+ private static final String SUFFIX_COUNTER = ".count";
+ private static final String SUFFIX_ERROR = ".error";
+ private static final String SUFFIX_REALM = ".realm";
+
+ public PolarisMetricRegistry(MeterRegistry meterRegistry) {
+ this.meterRegistry = meterRegistry;
+ }
+
+ public MeterRegistry getMeterRegistry() {
+ return meterRegistry;
+ }
+
+ public void init(Class>... classes) {
+ for (Class> clazz : classes) {
+ Method[] methods = clazz.getDeclaredMethods();
+ for (Method method : methods) {
+ if (method.isAnnotationPresent(TimedApi.class)) {
+ TimedApi timedApi = method.getAnnotation(TimedApi.class);
+ String metric = timedApi.value();
+ timers.put(metric, Timer.builder(metric).register(meterRegistry));
+ counters.put(
+ metric + SUFFIX_COUNTER,
+ Counter.builder(metric + SUFFIX_COUNTER).register(meterRegistry));
+
+ // Error counters contain the HTTP response code in a tag, thus caching them would not be
+ // meaningful.
+ Counter.builder(metric + SUFFIX_ERROR).tags(TAG_RESP_CODE, "400").register(meterRegistry);
+ Counter.builder(metric + SUFFIX_ERROR).tags(TAG_RESP_CODE, "500").register(meterRegistry);
+ }
+ }
+ }
+ }
+
+ public void recordTimer(String metric, long elapsedTimeMs, String realmId) {
+ Timer timer =
+ timers.computeIfAbsent(metric, m -> Timer.builder(metric).register(meterRegistry));
+ timer.record(elapsedTimeMs, TimeUnit.MILLISECONDS);
+
+ Timer timerRealm =
+ timers.computeIfAbsent(
+ metric + SUFFIX_REALM,
+ m ->
+ Timer.builder(metric + SUFFIX_REALM)
+ .tag(TAG_REALM, realmId)
+ .register(meterRegistry));
+ timerRealm.record(elapsedTimeMs, TimeUnit.MILLISECONDS);
+ }
+
+ public void incrementCounter(String metric, String realmId) {
+ String counterMetric = metric + SUFFIX_COUNTER;
+ Counter counter =
+ counters.computeIfAbsent(
+ counterMetric, m -> Counter.builder(counterMetric).register(meterRegistry));
+ counter.increment();
+
+ Counter counterRealm =
+ counters.computeIfAbsent(
+ counterMetric + SUFFIX_REALM,
+ m ->
+ Counter.builder(counterMetric + SUFFIX_REALM)
+ .tag(TAG_REALM, realmId)
+ .register(meterRegistry));
+ counterRealm.increment();
+ }
+
+ public void incrementErrorCounter(String metric, int statusCode, String realmId) {
+ String errorMetric = metric + SUFFIX_ERROR;
+ Counter.builder(errorMetric)
+ .tag(TAG_RESP_CODE, String.valueOf(statusCode))
+ .register(meterRegistry)
+ .increment();
+
+ Counter.builder(errorMetric + SUFFIX_REALM)
+ .tag(TAG_RESP_CODE, String.valueOf(statusCode))
+ .tag(TAG_REALM, realmId)
+ .register(meterRegistry)
+ .increment();
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java b/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java
new file mode 100644
index 0000000000..70a92331a8
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.persistence;
+
+import io.polaris.core.PolarisCallContext;
+import io.polaris.core.PolarisDefaultDiagServiceImpl;
+import io.polaris.core.PolarisDiagnostics;
+import io.polaris.core.context.CallContext;
+import io.polaris.core.context.RealmContext;
+import io.polaris.core.entity.PolarisEntity;
+import io.polaris.core.entity.PolarisEntityConstants;
+import io.polaris.core.entity.PolarisEntitySubType;
+import io.polaris.core.entity.PolarisEntityType;
+import io.polaris.core.entity.PolarisPrincipalSecrets;
+import io.polaris.core.monitor.PolarisMetricRegistry;
+import io.polaris.core.storage.PolarisStorageIntegrationProvider;
+import io.polaris.core.storage.cache.StorageCredentialCache;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+
+/**
+ * The common implementation of Configuration interface for configuring the {@link
+ * PolarisMetaStoreManager} using an underlying meta store to store and retrieve all Polaris
+ * metadata.
+ */
+public abstract class LocalPolarisMetaStoreManagerFactory<
+ StoreType, SessionType extends PolarisMetaStoreSession>
+ implements MetaStoreManagerFactory {
+
+ Map metaStoreManagerMap = new HashMap<>();
+ Map storageCredentialCacheMap = new HashMap<>();
+ Map backingStoreMap = new HashMap<>();
+ Map> sessionSupplierMap = new HashMap<>();
+ protected PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl();
+
+ protected PolarisStorageIntegrationProvider storageIntegration;
+
+ private Logger logger =
+ org.slf4j.LoggerFactory.getLogger(LocalPolarisMetaStoreManagerFactory.class);
+
+ protected abstract StoreType createBackingStore(@NotNull PolarisDiagnostics diagnostics);
+
+ protected abstract PolarisMetaStoreSession createMetaStoreSession(
+ @NotNull StoreType store, @NotNull RealmContext realmContext);
+
+ private void initializeForRealm(RealmContext realmContext) {
+ final StoreType backingStore = createBackingStore(diagServices);
+ backingStoreMap.put(realmContext.getRealmIdentifier(), backingStore);
+ sessionSupplierMap.put(
+ realmContext.getRealmIdentifier(),
+ () -> createMetaStoreSession(backingStore, realmContext));
+
+ PolarisMetaStoreManager metaStoreManager = new PolarisMetaStoreManagerImpl();
+ metaStoreManagerMap.put(realmContext.getRealmIdentifier(), metaStoreManager);
+ }
+
+ @Override
+ public synchronized Map bootstrapRealms(
+ List realms) {
+ Map results = new HashMap<>();
+
+ for (String realm : realms) {
+ RealmContext realmContext = () -> realm;
+ if (!metaStoreManagerMap.containsKey(realmContext.getRealmIdentifier())) {
+ initializeForRealm(realmContext);
+ PolarisMetaStoreManager.PrincipalSecretsResult secretsResult =
+ bootstrapServiceAndCreatePolarisPrincipalForRealm(
+ realmContext, metaStoreManagerMap.get(realmContext.getRealmIdentifier()));
+ results.put(realmContext.getRealmIdentifier(), secretsResult);
+ }
+ }
+
+ return results;
+ }
+
+ @Override
+ public synchronized PolarisMetaStoreManager getOrCreateMetaStoreManager(
+ RealmContext realmContext) {
+ if (!metaStoreManagerMap.containsKey(realmContext.getRealmIdentifier())) {
+ initializeForRealm(realmContext);
+ checkPolarisServiceBootstrappedForRealm(
+ realmContext, metaStoreManagerMap.get(realmContext.getRealmIdentifier()));
+ }
+ return metaStoreManagerMap.get(realmContext.getRealmIdentifier());
+ }
+
+ @Override
+ public synchronized Supplier getOrCreateSessionSupplier(
+ RealmContext realmContext) {
+ if (!sessionSupplierMap.containsKey(realmContext.getRealmIdentifier())) {
+ initializeForRealm(realmContext);
+ checkPolarisServiceBootstrappedForRealm(
+ realmContext, metaStoreManagerMap.get(realmContext.getRealmIdentifier()));
+ }
+ return sessionSupplierMap.get(realmContext.getRealmIdentifier());
+ }
+
+ @Override
+ public synchronized StorageCredentialCache getOrCreateStorageCredentialCache(
+ RealmContext realmContext) {
+ if (!storageCredentialCacheMap.containsKey(realmContext.getRealmIdentifier())) {
+ storageCredentialCacheMap.put(
+ realmContext.getRealmIdentifier(), new StorageCredentialCache());
+ }
+
+ return storageCredentialCacheMap.get(realmContext.getRealmIdentifier());
+ }
+
+ @Override
+ public void setMetricRegistry(PolarisMetricRegistry metricRegistry) {
+ // no-op
+ }
+
+ @Override
+ public void setStorageIntegrationProvider(PolarisStorageIntegrationProvider storageIntegration) {
+ this.storageIntegration = storageIntegration;
+ }
+
+ /**
+ * This method bootstraps service for a given realm: i.e. creates all the needed entities in the
+ * metastore and creates a root service principal. After that we rotate the root principal
+ * credentials and print them to stdout
+ *
+ * @param realmContext
+ * @param metaStoreManager
+ */
+ private PolarisMetaStoreManager.PrincipalSecretsResult
+ bootstrapServiceAndCreatePolarisPrincipalForRealm(
+ RealmContext realmContext, PolarisMetaStoreManager metaStoreManager) {
+ // While bootstrapping we need to act as a fake privileged context since the real
+ // CallContext hasn't even been resolved yet.
+ PolarisCallContext polarisContext =
+ new PolarisCallContext(
+ sessionSupplierMap.get(realmContext.getRealmIdentifier()).get(), diagServices);
+ CallContext.setCurrentContext(CallContext.of(realmContext, polarisContext));
+
+ metaStoreManager.bootstrapPolarisService(polarisContext);
+
+ PolarisMetaStoreManager.EntityResult rootPrincipalLookup =
+ metaStoreManager.readEntityByName(
+ polarisContext,
+ null,
+ PolarisEntityType.PRINCIPAL,
+ PolarisEntitySubType.NULL_SUBTYPE,
+ PolarisEntityConstants.getRootPrincipalName());
+ PolarisPrincipalSecrets secrets =
+ metaStoreManager
+ .loadPrincipalSecrets(
+ polarisContext,
+ PolarisEntity.of(rootPrincipalLookup.getEntity())
+ .getInternalPropertiesAsMap()
+ .get(PolarisEntityConstants.getClientIdPropertyName()))
+ .getPrincipalSecrets();
+ PolarisMetaStoreManager.PrincipalSecretsResult rotatedSecrets =
+ metaStoreManager.rotatePrincipalSecrets(
+ polarisContext,
+ secrets.getPrincipalClientId(),
+ secrets.getPrincipalId(),
+ secrets.getMainSecret(),
+ false);
+ return rotatedSecrets;
+ }
+
+ /**
+ * In this method we check if Service was bootstrapped for a given realm, i.e. that all the
+ * entities were created (root principal, root principal role, etc) If service was not
+ * bootstrapped we are throwing IllegalStateException exception That will cause service to crash
+ * and force user to run Bootstrap command and initialize MetaStore and create all the required
+ * entities
+ *
+ * @param realmContext
+ * @param metaStoreManager
+ */
+ private void checkPolarisServiceBootstrappedForRealm(
+ RealmContext realmContext, PolarisMetaStoreManager metaStoreManager) {
+ PolarisCallContext polarisContext =
+ new PolarisCallContext(
+ sessionSupplierMap.get(realmContext.getRealmIdentifier()).get(), diagServices);
+ CallContext.setCurrentContext(CallContext.of(realmContext, polarisContext));
+
+ PolarisMetaStoreManager.EntityResult rootPrincipalLookup =
+ metaStoreManager.readEntityByName(
+ polarisContext,
+ null,
+ PolarisEntityType.PRINCIPAL,
+ PolarisEntitySubType.NULL_SUBTYPE,
+ PolarisEntityConstants.getRootPrincipalName());
+
+ if (!rootPrincipalLookup.isSuccess()) {
+ logger.error(
+ "\n\n Realm {} is not bootstrapped, could not load root principal. Please run Bootstrap command. \n\n",
+ realmContext.getRealmIdentifier());
+ throw new IllegalStateException(
+ "Realm is not bootstrapped, please run server in bootstrap mode.");
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java b/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java
new file mode 100644
index 0000000000..199778dedb
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.persistence;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import io.dropwizard.jackson.Discoverable;
+import io.polaris.core.context.RealmContext;
+import io.polaris.core.monitor.PolarisMetricRegistry;
+import io.polaris.core.storage.PolarisStorageIntegrationProvider;
+import io.polaris.core.storage.cache.StorageCredentialCache;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * Configuration interface for configuring the {@link PolarisMetaStoreManager} via Dropwizard
+ * configuration
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
+public interface MetaStoreManagerFactory extends Discoverable {
+
+ PolarisMetaStoreManager getOrCreateMetaStoreManager(RealmContext realmContext);
+
+ Supplier getOrCreateSessionSupplier(RealmContext realmContext);
+
+ StorageCredentialCache getOrCreateStorageCredentialCache(RealmContext realmContext);
+
+ void setStorageIntegrationProvider(PolarisStorageIntegrationProvider storageIntegrationProvider);
+
+ void setMetricRegistry(PolarisMetricRegistry metricRegistry);
+
+ Map bootstrapRealms(List realms);
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityManager.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityManager.java
new file mode 100644
index 0000000000..79739813da
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityManager.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.persistence;
+
+import io.polaris.core.auth.AuthenticatedPolarisPrincipal;
+import io.polaris.core.context.CallContext;
+import io.polaris.core.entity.PolarisEntity;
+import io.polaris.core.entity.PolarisEntityConstants;
+import io.polaris.core.entity.PolarisEntitySubType;
+import io.polaris.core.entity.PolarisEntityType;
+import io.polaris.core.entity.PolarisGrantRecord;
+import io.polaris.core.entity.PolarisPrivilege;
+import io.polaris.core.persistence.cache.EntityCache;
+import io.polaris.core.persistence.resolver.PolarisResolutionManifest;
+import io.polaris.core.persistence.resolver.Resolver;
+import io.polaris.core.storage.cache.StorageCredentialCache;
+import java.util.List;
+import java.util.function.Supplier;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Wraps logic of handling name-caching and entity-caching against a concrete underlying entity
+ * store while exposing methods more natural for the Catalog layer to use. Encapsulates the various
+ * id and name resolution mechanics around PolarisEntities.
+ */
+public class PolarisEntityManager {
+ private static final Logger LOG = LoggerFactory.getLogger(PolarisEntityManager.class);
+
+ private final PolarisMetaStoreManager metaStoreManager;
+ private final Supplier sessionSupplier;
+ private final EntityCache entityCache;
+
+ private final StorageCredentialCache credentialCache;
+
+ // Lazily instantiated only a single time per entity manager.
+ private ResolvedPolarisEntity implicitResolvedRootContainerEntity = null;
+
+ /**
+ * @param sessionSupplier must return a new independent metastore session affiliated with the
+ * backing store under the {@code delegate} on each invocation.
+ */
+ public PolarisEntityManager(
+ PolarisMetaStoreManager metaStoreManager,
+ Supplier sessionSupplier,
+ StorageCredentialCache credentialCache) {
+ this.metaStoreManager = metaStoreManager;
+ this.sessionSupplier = sessionSupplier;
+ this.entityCache = new EntityCache(metaStoreManager);
+ this.credentialCache = credentialCache;
+ }
+
+ public PolarisMetaStoreSession newMetaStoreSession() {
+ return sessionSupplier.get();
+ }
+
+ public PolarisMetaStoreManager getMetaStoreManager() {
+ return metaStoreManager;
+ }
+
+ public Resolver prepareResolver(
+ @NotNull CallContext callContext,
+ @NotNull AuthenticatedPolarisPrincipal authenticatedPrincipal,
+ @Nullable String referenceCatalogName) {
+ return new Resolver(
+ callContext.getPolarisCallContext(),
+ metaStoreManager,
+ authenticatedPrincipal.getPrincipalEntity().getId(),
+ null, /* callerPrincipalName */
+ authenticatedPrincipal.getActivatedPrincipalRoleNames().isEmpty()
+ ? null
+ : authenticatedPrincipal.getActivatedPrincipalRoleNames(),
+ entityCache,
+ referenceCatalogName);
+ }
+
+ public PolarisResolutionManifest prepareResolutionManifest(
+ @NotNull CallContext callContext,
+ @NotNull AuthenticatedPolarisPrincipal authenticatedPrincipal,
+ @Nullable String referenceCatalogName) {
+ PolarisResolutionManifest manifest =
+ new PolarisResolutionManifest(
+ callContext, this, authenticatedPrincipal, referenceCatalogName);
+ manifest.setSimulatedResolvedRootContainerEntity(
+ getSimulatedResolvedRootContainerEntity(callContext));
+ return manifest;
+ }
+
+ /**
+ * Returns a ResolvedPolarisEntity representing the realm-level "root" entity that is the implicit
+ * parent container of all things in this realm.
+ */
+ private synchronized ResolvedPolarisEntity getSimulatedResolvedRootContainerEntity(
+ CallContext callContext) {
+ if (implicitResolvedRootContainerEntity == null) {
+ // For now, the root container is only implicit and doesn't exist in the entity store, and
+ // only
+ // the service_admin PrincipalRole has the SERVICE_MANAGE_ACCESS grant on this entity. If it
+ // becomes
+ // possible to grant other PrincipalRoles with SERVICE_MANAGE_ACCESS or other privileges on
+ // this
+ // root entity, then we must actually create a representation of this root entity in the
+ // entity store itself.
+ PolarisEntity serviceAdminPrincipalRole =
+ PolarisEntity.of(
+ metaStoreManager
+ .readEntityByName(
+ callContext.getPolarisCallContext(),
+ null,
+ PolarisEntityType.PRINCIPAL_ROLE,
+ PolarisEntitySubType.NULL_SUBTYPE,
+ PolarisEntityConstants.getNameOfPrincipalServiceAdminRole())
+ .getEntity());
+ if (serviceAdminPrincipalRole == null) {
+ throw new IllegalStateException("Failed to resolve service_admin PrincipalRole");
+ }
+ PolarisEntity rootContainerEntity =
+ new PolarisEntity.Builder()
+ .setId(0L)
+ .setCatalogId(0L)
+ .setType(PolarisEntityType.ROOT)
+ .setName("root")
+ .build();
+ PolarisGrantRecord serviceAdminGrant =
+ new PolarisGrantRecord(
+ 0L,
+ 0L,
+ serviceAdminPrincipalRole.getCatalogId(),
+ serviceAdminPrincipalRole.getId(),
+ PolarisPrivilege.SERVICE_MANAGE_ACCESS.getCode());
+
+ implicitResolvedRootContainerEntity =
+ new ResolvedPolarisEntity(rootContainerEntity, null, List.of(serviceAdminGrant));
+ }
+ return implicitResolvedRootContainerEntity;
+ }
+
+ public StorageCredentialCache getCredentialCache() {
+ return credentialCache;
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityResolver.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityResolver.java
new file mode 100644
index 0000000000..a5a87731b5
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityResolver.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.persistence;
+
+import io.polaris.core.PolarisCallContext;
+import io.polaris.core.PolarisDiagnostics;
+import io.polaris.core.entity.PolarisBaseEntity;
+import io.polaris.core.entity.PolarisEntitiesActiveKey;
+import io.polaris.core.entity.PolarisEntityActiveRecord;
+import io.polaris.core.entity.PolarisEntityConstants;
+import io.polaris.core.entity.PolarisEntityCore;
+import io.polaris.core.entity.PolarisEntityType;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Utility class used by the meta store manager to ensure that all entities which had been resolved
+ * by the Polaris service outside a transaction have not been changed by a concurrent operation. In
+ * particular, we will ensure that all entities resolved outside the transaction are still active,
+ * have not been renamed/re-parented or replaced by another entity with the same name.
+ */
+public class PolarisEntityResolver {
+
+ // cache diagnostics services
+ private final PolarisDiagnostics diagnostics;
+
+ // result of the resolution
+ private final boolean isSuccess;
+
+ // the catalog entity on the path. Only set if a catalog path is specified, i.e. if the entity
+ // being resolved is contain within a top-level catalog
+ private final PolarisEntityCore catalogEntity;
+
+ // the parent id of the entity. We have 2 cases here:
+ // - a path was specified, in which case the parent is the last element in that path
+ // - a path was not specified, in which case the parent id is the account.
+ private final long parentEntityId;
+
+ /**
+ * Full constructor for the resolver. The caller can specify a path inside a catalog which MUST
+ * start with the catalog itself. Then an optional entity to also resolve. This entity will be
+ * top-level if the catalogPath is null, else it will be under that path. Finally, the caller can
+ * specify other top-level entities to resolve, either catalog or account top-level. If a catalog
+ * top-level entity is specified, the catalogPath should be specified in order to know the parent
+ * catalog.
+ *
+ * The resolver will ensure that none of the entities which are passed in have been dropped or
+ * were renamed or moved.
+ *
+ * @param callCtx call context
+ * @param ms meta store in read mode
+ * @param catalogPath path within the catalog. The first element MUST be a catalog entity.
+ * @param resolvedEntity optional entity to resolve under that catalog path. If a non-null value
+ * is supplied, we will resolve it with the rest, as if it had been concatenated to the input
+ * path. If catalogPath is null, this MUST be a top-level entity
+ * @param otherTopLevelEntities any other top-level entities like a catalog role, a principal role
+ * or a principal can be specified here
+ */
+ PolarisEntityResolver(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable List catalogPath,
+ @Nullable PolarisEntityCore resolvedEntity,
+ @Nullable List otherTopLevelEntities) {
+
+ // cache diagnostics services
+ this.diagnostics = callCtx.getDiagServices();
+
+ // validate path if one was specified
+ if (catalogPath != null) {
+ // cannot be an empty list
+ callCtx.getDiagServices().check(!catalogPath.isEmpty(), "catalogPath_cannot_be_empty");
+ // first in the path should be the catalog
+ callCtx
+ .getDiagServices()
+ .check(
+ catalogPath.get(0).getTypeCode() == PolarisEntityType.CATALOG.getCode(),
+ "entity_is_not_catalog",
+ "entity={}",
+ this);
+ } else if (resolvedEntity != null) {
+ // if an entity is specified without any path, it better be a top-level entity
+ callCtx
+ .getDiagServices()
+ .check(
+ resolvedEntity.getType().isTopLevel(),
+ "not_top_level_entity",
+ "resolvedEntity={}",
+ resolvedEntity);
+ }
+
+ // validate the otherTopLevelCatalogEntities list. Must be top-level catalog entities
+ if (otherTopLevelEntities != null) {
+ // ensure all entities are top-level
+ for (PolarisEntityCore topLevelCatalogEntityDto : otherTopLevelEntities) {
+ // top-level (catalog or account) and is catalog, catalog path must be specified
+ callCtx
+ .getDiagServices()
+ .check(
+ topLevelCatalogEntityDto.isTopLevel()
+ || (topLevelCatalogEntityDto.getType().getParentType()
+ == PolarisEntityType.CATALOG
+ && catalogPath != null),
+ "not_top_level_or_missing_catalog_path",
+ "entity={} catalogPath={}",
+ topLevelCatalogEntityDto,
+ catalogPath);
+ }
+ }
+
+ // call the resolution logic
+ this.isSuccess =
+ this.resolveEntitiesIfNeeded(
+ callCtx, ms, catalogPath, resolvedEntity, otherTopLevelEntities);
+
+ // process result
+ if (!this.isSuccess) {
+ // if failed, initialized if NA values
+ this.catalogEntity = null;
+ this.parentEntityId = PolarisEntityConstants.getNullId();
+ } else if (catalogPath != null) {
+ this.catalogEntity = catalogPath.get(0);
+ this.parentEntityId = catalogPath.get(catalogPath.size() - 1).getId();
+ } else {
+ this.catalogEntity = null;
+ this.parentEntityId = PolarisEntityConstants.getRootEntityId();
+ }
+ }
+
+ /**
+ * Constructor for the resolver, when we only need to resolve a path
+ *
+ * @param callCtx call context
+ * @param ms meta store in read mode
+ * @param catalogPath input path, can be null or empty list if the entity is a top-level entity
+ * like a catalog.
+ */
+ PolarisEntityResolver(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable List catalogPath) {
+ this(callCtx, ms, catalogPath, null, null);
+ }
+
+ /**
+ * Constructor for the resolver, when we only need to resolve a path
+ *
+ * @param callCtx call context
+ * @param ms meta store in read mode
+ * @param catalogPath input path, can be null or empty list if the entity is a top-level entity
+ * like a catalog.
+ * @param resolvedEntityDto resolved entity DTO
+ */
+ PolarisEntityResolver(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable List catalogPath,
+ PolarisEntityCore resolvedEntityDto) {
+ this(callCtx, ms, catalogPath, resolvedEntityDto, null);
+ }
+
+ /**
+ * Constructor for the resolver, when we only need to resolve a path
+ *
+ * @param callCtx call context
+ * @param ms meta store in read mode
+ * @param catalogPath input path, can be null or empty list if the entity is a top-level entity
+ * like a catalog.
+ * @param entity Polaris base entity
+ */
+ PolarisEntityResolver(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable List catalogPath,
+ @NotNull PolarisBaseEntity entity) {
+ this(callCtx, ms, catalogPath, new PolarisEntityCore(entity), null);
+ }
+
+ /**
+ * @return status of the resolution, if true we couldn't resolve everything
+ */
+ boolean isFailure() {
+ return !this.isSuccess;
+ }
+
+ /**
+ * @return If a non-null catalog path was specified at construction time, the id of the last
+ * entity in this path, else the pseudo account id, i.e. 0
+ */
+ long getParentId() {
+ this.diagnostics.check(this.isSuccess, "resolver_failed");
+ return this.parentEntityId;
+ }
+
+ /**
+ * @return id of the catalog or the "NULL" id if the entity is top-level
+ */
+ long getCatalogIdOrNull() {
+ this.diagnostics.check(this.isSuccess, "resolver_failed");
+ return this.catalogEntity == null
+ ? PolarisEntityConstants.getNullId()
+ : this.catalogEntity.getId();
+ }
+
+ /**
+ * Ensure all specified entities are still active, have not been renamed or re-parented.
+ *
+ * @param callCtx call context
+ * @param ms meta store in read mode
+ * @param catalogPath path within the catalog. The first element MUST be a catalog. Null or empty
+ * for top-level entities like catalog
+ * @param resolvedEntity optional entity to resolve under that catalog path. If a non-null value
+ * is supplied, we will resolve it with the rest, as if it had been concatenated to the input
+ * path.
+ * @param otherTopLevelEntities if non-null, these are top-level catalog entities under the
+ * catalog rooting the catalogPath. Hence, this can be specified only if catalogPath is not
+ * null
+ * @return true if all entities have been resolved successfully
+ */
+ private boolean resolveEntitiesIfNeeded(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable List catalogPath,
+ @Nullable PolarisEntityCore resolvedEntity,
+ @Nullable List otherTopLevelEntities) {
+
+ // determine the number of entities to resolved
+ int resolveCount =
+ ((catalogPath != null) ? catalogPath.size() : 0)
+ + ((resolvedEntity != null) ? 1 : 0)
+ + ((otherTopLevelEntities != null) ? otherTopLevelEntities.size() : 0);
+
+ // nothing to do if 0
+ if (resolveCount == 0) {
+ return true;
+ }
+
+ // construct full list of entities to resolve
+ final List toResolve = new ArrayList<>(resolveCount);
+
+ // first add the other top-level catalog entities, then the catalog path, then the entity
+ if (otherTopLevelEntities != null) {
+ toResolve.addAll(otherTopLevelEntities);
+ }
+ if (catalogPath != null) {
+ toResolve.addAll(catalogPath);
+ }
+ if (resolvedEntity != null) {
+ toResolve.add(resolvedEntity);
+ }
+
+ // now build a list of entity active keys
+ List entityActiveKeys =
+ toResolve.stream()
+ .map(
+ entityCore ->
+ new PolarisEntitiesActiveKey(
+ entityCore.getCatalogId(),
+ entityCore.getParentId(),
+ entityCore.getTypeCode(),
+ entityCore.getName()))
+ .collect(Collectors.toList());
+
+ // now lookup all these entities by name
+ Iterator activeRecordIt =
+ ms.lookupEntityActiveBatch(callCtx, entityActiveKeys).iterator();
+
+ // now validate if there was a change and if yes, re-resolve again
+ for (PolarisEntityCore resolveEntity : toResolve) {
+ // get associate active record
+ PolarisEntityActiveRecord activeEntityRecord = activeRecordIt.next();
+
+ // if this entity has been dropped (null) or replaced (<> ids), then fail validation
+ if (activeEntityRecord == null || activeEntityRecord.getId() != resolveEntity.getId()) {
+ return false;
+ }
+ }
+
+ // all good, everything was resolved successfully
+ return true;
+ }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManager.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManager.java
new file mode 100644
index 0000000000..278dea10af
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManager.java
@@ -0,0 +1,1482 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.persistence;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.polaris.core.PolarisCallContext;
+import io.polaris.core.entity.PolarisBaseEntity;
+import io.polaris.core.entity.PolarisChangeTrackingVersions;
+import io.polaris.core.entity.PolarisEntity;
+import io.polaris.core.entity.PolarisEntityActiveRecord;
+import io.polaris.core.entity.PolarisEntityCore;
+import io.polaris.core.entity.PolarisEntityId;
+import io.polaris.core.entity.PolarisEntitySubType;
+import io.polaris.core.entity.PolarisEntityType;
+import io.polaris.core.entity.PolarisGrantRecord;
+import io.polaris.core.entity.PolarisPrincipalSecrets;
+import io.polaris.core.entity.PolarisPrivilege;
+import io.polaris.core.storage.PolarisCredentialProperty;
+import io.polaris.core.storage.PolarisStorageActions;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Polaris Metastore Manager manages all Polaris entities and associated grant records metadata for
+ * authorization. It uses the underlying persistent metastore to store and retrieve Polaris metadata
+ */
+public interface PolarisMetaStoreManager {
+
+ /** Possible return code for the various API calls. */
+ enum ReturnStatus {
+ // all good
+ SUCCESS(1),
+
+ // an unexpected error was thrown, should result in a 500 error to the client
+ UNEXPECTED_ERROR_SIGNALED(2),
+
+ // the specified catalog path cannot be resolved. There is a possibility that by the time a call
+ // is made by the client to the persistent storage, something has changed due to concurrent
+ // modification(s). The client should retry in that case.
+ CATALOG_PATH_CANNOT_BE_RESOLVED(3),
+
+ // the specified entity (and its path) cannot be resolved. There is a possibility that by the
+ // time a call is made by the client to the persistent storage, something has changed due to
+ // concurrent modification(s). The client should retry in that case.
+ ENTITY_CANNOT_BE_RESOLVED(4),
+
+ // entity not found
+ ENTITY_NOT_FOUND(5),
+
+ // grant not found
+ GRANT_NOT_FOUND(6),
+
+ // entity already exists
+ ENTITY_ALREADY_EXISTS(7),
+
+ // entity cannot be dropped, it is one of the bootstrap object like a catalog admin role or the
+ // service admin principal role
+ ENTITY_UNDROPPABLE(8),
+
+ // Namespace is not empty and cannot be dropped
+ NAMESPACE_NOT_EMPTY(9),
+
+ // Catalog is not empty and cannot be dropped. All catalog roles (except the admin catalog
+ // role) and all namespaces in the catalog must be dropped before the namespace can be dropped
+ CATALOG_NOT_EMPTY(10),
+
+ // The target entity was concurrently modified
+ TARGET_ENTITY_CONCURRENTLY_MODIFIED(11),
+
+ // entity cannot be renamed
+ ENTITY_CANNOT_BE_RENAMED(12),
+
+ // error caught while sub-scoping credentials. Error message will be returned
+ SUBSCOPE_CREDS_ERROR(13),
+ ;
+
+ // code for the enum
+ private final int code;
+
+ /** constructor */
+ ReturnStatus(int code) {
+ this.code = code;
+ }
+
+ int getCode() {
+ return this.code;
+ }
+
+ // to efficiently map a code to its corresponding return status
+ private static final ReturnStatus[] REVERSE_MAPPING_ARRAY;
+
+ static {
+ // find max array size
+ int maxCode = 0;
+ for (ReturnStatus returnStatus : ReturnStatus.values()) {
+ if (maxCode < returnStatus.code) {
+ maxCode = returnStatus.code;
+ }
+ }
+
+ // allocate mapping array
+ REVERSE_MAPPING_ARRAY = new ReturnStatus[maxCode + 1];
+
+ // populate mapping array
+ for (ReturnStatus returnStatus : ReturnStatus.values()) {
+ REVERSE_MAPPING_ARRAY[returnStatus.code] = returnStatus;
+ }
+ }
+
+ static ReturnStatus getStatus(int code) {
+ return code >= REVERSE_MAPPING_ARRAY.length ? null : REVERSE_MAPPING_ARRAY[code];
+ }
+ }
+
+ /** Base result class for any call to the persistence layer */
+ class BaseResult {
+ // return code, indicates success or failure
+ private final int returnStatusCode;
+
+ // additional information for some error return code
+ private final String extraInformation;
+
+ public BaseResult() {
+ this.returnStatusCode = ReturnStatus.SUCCESS.getCode();
+ this.extraInformation = null;
+ }
+
+ public BaseResult(@NotNull PolarisMetaStoreManager.ReturnStatus returnStatus) {
+ this.returnStatusCode = returnStatus.getCode();
+ this.extraInformation = null;
+ }
+
+ @JsonCreator
+ public BaseResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") @Nullable String extraInformation) {
+ this.returnStatusCode = returnStatus.getCode();
+ this.extraInformation = extraInformation;
+ }
+
+ public ReturnStatus getReturnStatus() {
+ return ReturnStatus.getStatus(this.returnStatusCode);
+ }
+
+ public String getExtraInformation() {
+ return extraInformation;
+ }
+
+ public boolean isSuccess() {
+ return this.returnStatusCode == ReturnStatus.SUCCESS.getCode();
+ }
+
+ public boolean alreadyExists() {
+ return this.returnStatusCode == ReturnStatus.ENTITY_ALREADY_EXISTS.getCode();
+ }
+ }
+
+ /**
+ * Bootstrap the Polaris service, will remove ALL existing persisted entities, then will create
+ * the root catalog, root principal and associated service admin role.
+ *
+ * *************************** WARNING ************************
+ *
+ *
This will destroy whatever Polaris metadata exists in this account
+ *
+ * @param callCtx call context
+ * @return always success or unexpected error
+ */
+ @NotNull
+ BaseResult bootstrapPolarisService(@NotNull PolarisCallContext callCtx);
+
+ /** the return for an entity lookup call */
+ class EntityResult extends BaseResult {
+
+ // null if not success
+ private final PolarisBaseEntity entity;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information if error. Implementation specific
+ */
+ public EntityResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.entity = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param entity the entity being looked-up
+ */
+ public EntityResult(@NotNull PolarisBaseEntity entity) {
+ super(ReturnStatus.SUCCESS);
+ this.entity = entity;
+ }
+
+ /**
+ * Constructor for an object already exists error where the subtype of the existing entity is
+ * returned
+ *
+ * @param errorStatus error status, cannot be SUCCESS
+ * @param subTypeCode existing entity subtype code
+ */
+ public EntityResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorStatus, int subTypeCode) {
+ super(errorStatus, Integer.toString(subTypeCode));
+ this.entity = null;
+ }
+
+ /**
+ * For object already exist error, we use the extra information to serialize the subtype code of
+ * the existing object. Return the subtype
+ *
+ * @return object subtype or NULL (should not happen) if subtype code is missing or cannot be
+ * deserialized
+ */
+ @Nullable
+ public PolarisEntitySubType getAlreadyExistsEntitySubType() {
+ if (this.getExtraInformation() == null) {
+ return null;
+ } else {
+ int subTypeCode;
+ try {
+ subTypeCode = Integer.parseInt(this.getExtraInformation());
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ return PolarisEntitySubType.fromCode(subTypeCode);
+ }
+ }
+
+ @JsonCreator
+ private EntityResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") @Nullable String extraInformation,
+ @JsonProperty("entity") @Nullable PolarisBaseEntity entity) {
+ super(returnStatus, extraInformation);
+ this.entity = entity;
+ }
+
+ public PolarisBaseEntity getEntity() {
+ return entity;
+ }
+ }
+
+ /**
+ * Resolve an entity by name. Can be a top-level entity like a catalog or an entity inside a
+ * catalog like a namespace, a role, a table like entity, or a principal. If the entity is inside
+ * a catalog, the parameter catalogPath must be specified
+ *
+ * @param callCtx call context
+ * @param catalogPath path inside a catalog to that entity, rooted by the catalog. If null, the
+ * entity being resolved is a top-level account entity like a catalog.
+ * @param entityType entity type
+ * @param entitySubType entity subtype. Can be the special value ANY_SUBTYPE to match any
+ * subtypes. Else exact match on the subtype will be required.
+ * @param name name of the entity, cannot be null
+ * @return the result of the lookup operation. ENTITY_NOT_FOUND is returned if the specified
+ * entity is not found in the specified path. CONCURRENT_MODIFICATION_DETECTED_NEED_RETRY is
+ * returned if the specified catalog path cannot be resolved.
+ */
+ @NotNull
+ PolarisMetaStoreManager.EntityResult readEntityByName(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityType entityType,
+ @NotNull PolarisEntitySubType entitySubType,
+ @NotNull String name);
+
+ /** the return the result for a list entities call */
+ class ListEntitiesResult extends BaseResult {
+
+ // null if not success. Else the list of entities being returned
+ private final List entities;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public ListEntitiesResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.entities = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param entities list of entities being returned, implies success
+ */
+ public ListEntitiesResult(@NotNull List entities) {
+ super(ReturnStatus.SUCCESS);
+ this.entities = entities;
+ }
+
+ @JsonCreator
+ private ListEntitiesResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") String extraInformation,
+ @JsonProperty("entities") List entities) {
+ super(returnStatus, extraInformation);
+ this.entities = entities;
+ }
+
+ public List getEntities() {
+ return entities;
+ }
+ }
+
+ /**
+ * List all entities of the specified type under the specified catalogPath. If the catalogPath is
+ * null, listed entities will be top-level entities like catalogs.
+ *
+ * @param callCtx call context
+ * @param catalogPath path inside a catalog. If null or empty, the entities to list are top-level,
+ * like catalogs
+ * @param entityType entity type
+ * @param entitySubType entity subtype. Can be the special value ANY_SUBTYPE to match any subtype.
+ * Else exact match will be performed.
+ * @return all entities name, ids and subtype under the specified namespace.
+ */
+ @NotNull
+ ListEntitiesResult listEntities(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityType entityType,
+ @NotNull PolarisEntitySubType entitySubType);
+
+ /** the return for a generate new entity id */
+ class GenerateEntityIdResult extends BaseResult {
+
+ // null if not success
+ private final Long id;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public GenerateEntityIdResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.id = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param id the new id which was generated
+ */
+ public GenerateEntityIdResult(@NotNull Long id) {
+ super(ReturnStatus.SUCCESS);
+ this.id = id;
+ }
+
+ @JsonCreator
+ private GenerateEntityIdResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") @Nullable String extraInformation,
+ @JsonProperty("id") @Nullable Long id) {
+ super(returnStatus, extraInformation);
+ this.id = id;
+ }
+
+ public Long getId() {
+ return id;
+ }
+ }
+
+ /**
+ * Generate a new unique id that can be used by the Polaris client when it needs to create a new
+ * entity
+ *
+ * @param callCtx call context
+ * @return the newly created id, not expected to fail
+ */
+ @NotNull
+ GenerateEntityIdResult generateNewEntityId(@NotNull PolarisCallContext callCtx);
+
+ /** the return the result of a create-principal method */
+ class CreatePrincipalResult extends BaseResult {
+ // the principal which has been created. Null if error
+ private final PolarisBaseEntity principal;
+
+ // principal client identifier and associated secrets. Null if error
+ private final PolarisPrincipalSecrets principalSecrets;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public CreatePrincipalResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.principal = null;
+ this.principalSecrets = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param principal the principal
+ * @param principalSecrets and associated secret information
+ */
+ public CreatePrincipalResult(
+ @NotNull PolarisBaseEntity principal, @NotNull PolarisPrincipalSecrets principalSecrets) {
+ super(ReturnStatus.SUCCESS);
+ this.principal = principal;
+ this.principalSecrets = principalSecrets;
+ }
+
+ @JsonCreator
+ private CreatePrincipalResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") @Nullable String extraInformation,
+ @JsonProperty("principal") @NotNull PolarisBaseEntity principal,
+ @JsonProperty("principalSecrets") @NotNull PolarisPrincipalSecrets principalSecrets) {
+ super(returnStatus, extraInformation);
+ this.principal = principal;
+ this.principalSecrets = principalSecrets;
+ }
+
+ public PolarisBaseEntity getPrincipal() {
+ return principal;
+ }
+
+ public PolarisPrincipalSecrets getPrincipalSecrets() {
+ return principalSecrets;
+ }
+ }
+
+ /**
+ * Create a new principal. This not only creates the new principal entity but also generates a
+ * client_id/secret pair for this new principal.
+ *
+ * @param callCtx call context
+ * @param principal the principal entity to create
+ * @return the client_id/secret for the new principal which was created. Will return
+ * ENTITY_ALREADY_EXISTS if the principal already exists
+ */
+ @NotNull
+ CreatePrincipalResult createPrincipal(
+ @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity principal);
+
+ /** the result of load/rotate principal secrets */
+ class PrincipalSecretsResult extends BaseResult {
+
+ // principal client identifier and associated secrets. Null if error
+ private final PolarisPrincipalSecrets principalSecrets;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public PrincipalSecretsResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.principalSecrets = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param principalSecrets and associated secret information
+ */
+ public PrincipalSecretsResult(@NotNull PolarisPrincipalSecrets principalSecrets) {
+ super(ReturnStatus.SUCCESS);
+ this.principalSecrets = principalSecrets;
+ }
+
+ @JsonCreator
+ private PrincipalSecretsResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") @Nullable String extraInformation,
+ @JsonProperty("principalSecrets") @NotNull PolarisPrincipalSecrets principalSecrets) {
+ super(returnStatus, extraInformation);
+ this.principalSecrets = principalSecrets;
+ }
+
+ public PolarisPrincipalSecrets getPrincipalSecrets() {
+ return principalSecrets;
+ }
+ }
+
+ /**
+ * Load the principal secrets given the client_id.
+ *
+ * @param callCtx call context
+ * @param clientId principal client id
+ * @return the secrets associated to that principal, including the entity id of the principal
+ */
+ @NotNull
+ PrincipalSecretsResult loadPrincipalSecrets(
+ @NotNull PolarisCallContext callCtx, @NotNull String clientId);
+
+ /**
+ * Rotate secrets
+ *
+ * @param callCtx call context
+ * @param clientId principal client id
+ * @param principalId id of the principal
+ * @param mainSecret main secret for the principal
+ * @param reset true if the principal's secrets should be disabled and replaced with a one-time
+ * password. if the principal's secret is already a one-time password, this flag is
+ * automatically true
+ * @return the secrets associated to that principal amd the id of the principal
+ */
+ @NotNull
+ PrincipalSecretsResult rotatePrincipalSecrets(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull String clientId,
+ long principalId,
+ @NotNull String mainSecret,
+ boolean reset);
+
+ /** the return the result of a create-catalog method */
+ class CreateCatalogResult extends BaseResult {
+
+ // the catalog which has been created
+ private final PolarisBaseEntity catalog;
+
+ // its associated catalog admin role
+ private final PolarisBaseEntity catalogAdminRole;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public CreateCatalogResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.catalog = null;
+ this.catalogAdminRole = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param catalog the catalog
+ * @param catalogAdminRole and associated admin role
+ */
+ public CreateCatalogResult(
+ @NotNull PolarisBaseEntity catalog, @NotNull PolarisBaseEntity catalogAdminRole) {
+ super(ReturnStatus.SUCCESS);
+ this.catalog = catalog;
+ this.catalogAdminRole = catalogAdminRole;
+ }
+
+ @JsonCreator
+ private CreateCatalogResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") @Nullable String extraInformation,
+ @JsonProperty("catalog") @NotNull PolarisBaseEntity catalog,
+ @JsonProperty("catalogAdminRole") @NotNull PolarisBaseEntity catalogAdminRole) {
+ super(returnStatus, extraInformation);
+ this.catalog = catalog;
+ this.catalogAdminRole = catalogAdminRole;
+ }
+
+ public PolarisBaseEntity getCatalog() {
+ return catalog;
+ }
+
+ public PolarisBaseEntity getCatalogAdminRole() {
+ return catalogAdminRole;
+ }
+ }
+
+ /**
+ * Create a new catalog. This not only creates the new catalog entity but also the initial admin
+ * role required to admin this catalog. If inline storage integration property is provided, create
+ * a storage integration.
+ *
+ * @param callCtx call context
+ * @param catalog the catalog entity to create
+ * @param principalRoles once the catalog has been created, list of principal roles to grant its
+ * catalog_admin role to. If no principal role is specified, we will grant the catalog_admin
+ * role of the newly created catalog to the service admin role.
+ * @return if success, the catalog which was created and its admin role.
+ */
+ @NotNull
+ CreateCatalogResult createCatalog(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisBaseEntity catalog,
+ @NotNull List principalRoles);
+
+ /**
+ * Persist a newly created entity under the specified catalog path if specified, else this is a
+ * top-level entity. We will re-resolve the specified path to ensure nothing has changed since the
+ * Polaris app resolved the path. If the entity already exists with the same specified id, we will
+ * simply return it. This can happen when the client retries. If a catalogPath is specified and
+ * cannot be resolved, we will return null. And of course if another entity exists with the same
+ * name, we will fail and also return null.
+ *
+ * @param callCtx call context
+ * @param catalogPath path inside a catalog. If null, the entity to persist is assumed to be
+ * top-level.
+ * @param entity entity to write
+ * @return the newly created entity. If this entity was already created, we will simply return the
+ * already created entity. We will return null if a different entity with the same name exists
+ * or if the catalogPath couldn't be resolved. If null is returned, the client app should
+ * retry this operation.
+ */
+ @NotNull
+ EntityResult createEntityIfNotExists(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisBaseEntity entity);
+
+ /** a set of returned entities result */
+ class EntitiesResult extends BaseResult {
+
+ // null if not success. Else the list of entities being returned
+ private final List entities;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorStatus error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public EntitiesResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorStatus,
+ @Nullable String extraInformation) {
+ super(errorStatus, extraInformation);
+ this.entities = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param entities list of entities being returned, implies success
+ */
+ public EntitiesResult(@NotNull List entities) {
+ super(ReturnStatus.SUCCESS);
+ this.entities = entities;
+ }
+
+ @JsonCreator
+ private EntitiesResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") String extraInformation,
+ @JsonProperty("entities") List entities) {
+ super(returnStatus, extraInformation);
+ this.entities = entities;
+ }
+
+ public List getEntities() {
+ return entities;
+ }
+ }
+
+ /**
+ * Persist a batch of newly created entities under the specified catalog path if specified, else
+ * these are top-level entities. We will re-resolve the specified path to ensure nothing has
+ * changed since the Polaris app resolved the path. If any of the entities already exists with the
+ * same specified id, we will simply return it. This can happen when the client retries. If a
+ * catalogPath is specified and cannot be resolved, we will return null and none of the entities
+ * will be persisted. And of course if any entity conflicts with an existing entity with the same
+ * name, we will fail all entities and also return null.
+ *
+ * @param callCtx call context
+ * @param catalogPath path inside a catalog. If null, the entity to persist is assumed to be
+ * top-level.
+ * @param entities batch of entities to write
+ * @return the newly created entities. If the entities were already created, we will simply return
+ * the already created entity. We will return null if a different entity with the same name
+ * exists or if the catalogPath couldn't be resolved. If null is returned, the client app
+ * should retry this operation.
+ */
+ @NotNull
+ EntitiesResult createEntitiesIfNotExist(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull List extends PolarisBaseEntity> entities);
+
+ /**
+ * Update some properties of this entity assuming it can still be resolved the same way and itself
+ * has not changed. If this is not the case we will return false. Else we will update both the
+ * internal and visible properties and return true
+ *
+ * @param callCtx call context
+ * @param catalogPath path to that entity. Could be null if this entity is top-level
+ * @param entity entity to update, cannot be null
+ * @return the entity we updated or null if the client should retry
+ */
+ @NotNull
+ EntityResult updateEntityPropertiesIfNotChanged(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisBaseEntity entity);
+
+ /** Class to represent an entity with its path */
+ class EntityWithPath {
+ // path to that entity. Could be null if this entity is top-level
+ private final @NotNull List catalogPath;
+
+ // the base entity itself
+ private final @NotNull PolarisBaseEntity entity;
+
+ @JsonCreator
+ public EntityWithPath(
+ @JsonProperty("catalogPath") @NotNull List catalogPath,
+ @JsonProperty("entity") @NotNull PolarisBaseEntity entity) {
+ this.catalogPath = catalogPath;
+ this.entity = entity;
+ }
+
+ public @NotNull List getCatalogPath() {
+ return catalogPath;
+ }
+
+ public @NotNull PolarisBaseEntity getEntity() {
+ return entity;
+ }
+ }
+
+ /**
+ * This works exactly like {@link #updateEntityPropertiesIfNotChanged(PolarisCallContext, List,
+ * PolarisBaseEntity)} but allows to operate on multiple entities at once. Just loop through the
+ * list, calling each entity update and return null if any of those fail.
+ *
+ * @param callCtx call context
+ * @param entities the set of entities to update
+ * @return list of all entities we updated or null if the client should retry because one update
+ * failed
+ */
+ @NotNull
+ EntitiesResult updateEntitiesPropertiesIfNotChanged(
+ @NotNull PolarisCallContext callCtx, @NotNull List entities);
+
+ /**
+ * Rename an entity, potentially re-parenting it.
+ *
+ * @param callCtx call context
+ * @param catalogPath path to that entity. Could be an empty list of the entity is a catalog.
+ * @param entityToRename entity to rename. This entity should have been resolved by the client
+ * @param newCatalogPath if not null, new catalog path
+ * @param renamedEntity the new renamed entity we need to persist. We will use this argument to
+ * also update the internal and external properties as part of the rename operation. This is
+ * required to update the namespace path of the entity if it has changed
+ * @return the entity after renaming it or null if the rename operation has failed
+ */
+ @NotNull
+ EntityResult renameEntity(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityCore entityToRename,
+ @Nullable List newCatalogPath,
+ @NotNull PolarisEntity renamedEntity);
+
+ // the return the result of a drop entity
+ class DropEntityResult extends BaseResult {
+
+ /** If cleanup was requested and a task was successfully scheduled, */
+ private final Long cleanupTaskId;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorStatus error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public DropEntityResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorStatus,
+ @Nullable String extraInformation) {
+ super(errorStatus, extraInformation);
+ this.cleanupTaskId = null;
+ }
+
+ /** Constructor for success when no cleanup needs to be performed */
+ public DropEntityResult() {
+ super(ReturnStatus.SUCCESS);
+ this.cleanupTaskId = null;
+ }
+
+ /**
+ * Constructor for success when a cleanup task has been scheduled
+ *
+ * @param cleanupTaskId id of the task which was created to clean up the table drop
+ */
+ public DropEntityResult(long cleanupTaskId) {
+ super(ReturnStatus.SUCCESS);
+ this.cleanupTaskId = cleanupTaskId;
+ }
+
+ @JsonCreator
+ private DropEntityResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") String extraInformation,
+ @JsonProperty("cleanupTaskId") Long cleanupTaskId) {
+ super(returnStatus, extraInformation);
+ this.cleanupTaskId = cleanupTaskId;
+ }
+
+ public Long getCleanupTaskId() {
+ return cleanupTaskId;
+ }
+
+ @JsonIgnore
+ public boolean failedBecauseNotEmpty() {
+ ReturnStatus status = this.getReturnStatus();
+ return status == ReturnStatus.CATALOG_NOT_EMPTY || status == ReturnStatus.NAMESPACE_NOT_EMPTY;
+ }
+
+ public boolean isEntityUnDroppable() {
+ return this.getReturnStatus() == ReturnStatus.ENTITY_UNDROPPABLE;
+ }
+ }
+
+ /**
+ * Drop the specified entity assuming it exists
+ *
+ * @param callCtx call context
+ * @param catalogPath path to that entity. Could be an empty list of the entity is a catalog.
+ * @param entityToDrop entity to drop, must have been resolved by the client
+ * @param cleanupProperties if not null, properties that will be persisted with the cleanup task
+ * @param cleanup true if resources owned by this entity should be deleted as well
+ * @return the result of the drop entity call, either success or error. If the error, it could be
+ * that the namespace or catalog to drop still has children, this should not be retried and
+ * should cause a failure
+ */
+ @NotNull
+ DropEntityResult dropEntityIfExists(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityCore entityToDrop,
+ @Nullable Map cleanupProperties,
+ boolean cleanup);
+
+ /** Result of a grant/revoke privilege call */
+ class PrivilegeResult extends BaseResult {
+
+ // null if not success.
+ private final PolarisGrantRecord grantRecord;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public PrivilegeResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.grantRecord = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param grantRecord grant record being granted or revoked
+ */
+ public PrivilegeResult(@NotNull PolarisGrantRecord grantRecord) {
+ super(ReturnStatus.SUCCESS);
+ this.grantRecord = grantRecord;
+ }
+
+ @JsonCreator
+ private PrivilegeResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") String extraInformation,
+ @JsonProperty("grantRecord") PolarisGrantRecord grantRecord) {
+ super(returnStatus, extraInformation);
+ this.grantRecord = grantRecord;
+ }
+
+ public PolarisGrantRecord getGrantRecord() {
+ return grantRecord;
+ }
+ }
+
+ /**
+ * Grant usage on a role to a grantee, for example granting usage on a catalog role to a principal
+ * role or granting a principal role to a principal.
+ *
+ * @param callCtx call context
+ * @param catalog if the role is a catalog role, the caller needs to pass-in the catalog entity
+ * which was used to resolve that granted. Else null.
+ * @param role resolved catalog or principal role
+ * @param grantee principal role or principal as resolved by the caller
+ * @return the grant record we created for this grant. Will return ENTITY_NOT_FOUND if the
+ * specified role couldn't be found. Should be retried in that case
+ */
+ @NotNull
+ PrivilegeResult grantUsageOnRoleToGrantee(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable PolarisEntityCore catalog,
+ @NotNull PolarisEntityCore role,
+ @NotNull PolarisEntityCore grantee);
+
+ /**
+ * Revoke usage on a role (a catalog or a principal role) from a grantee (e.g. a principal role or
+ * a principal).
+ *
+ * @param callCtx call context
+ * @param catalog if the granted is a catalog role, the caller needs to pass-in the catalog entity
+ * which was used to resolve that role. Else null should be passed-in.
+ * @param role a catalog/principal role as resolved by the caller
+ * @param grantee resolved principal role or principal
+ * @return the result. Will return ENTITY_NOT_FOUND if the * specified role couldn't be found.
+ * Should be retried in that case. Will return GRANT_NOT_FOUND if the grant to revoke cannot
+ * be found
+ */
+ @NotNull
+ PrivilegeResult revokeUsageOnRoleFromGrantee(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable PolarisEntityCore catalog,
+ @NotNull PolarisEntityCore role,
+ @NotNull PolarisEntityCore grantee);
+
+ /**
+ * Grant a privilege on a catalog securable to a grantee.
+ *
+ * @param callCtx call context
+ * @param grantee resolved role, the grantee
+ * @param catalogPath path to that entity, cannot be null or empty unless securable is top-level
+ * @param securable securable entity, must have been resolved by the client. Can be the catalog
+ * itself
+ * @param privilege privilege to grant
+ * @return the grant record we created for this grant. Will return ENTITY_NOT_FOUND if the
+ * specified role couldn't be found. Should be retried in that case
+ */
+ @NotNull
+ PrivilegeResult grantPrivilegeOnSecurableToRole(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisEntityCore grantee,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityCore securable,
+ @NotNull PolarisPrivilege privilege);
+
+ /**
+ * Revoke a privilege on a catalog securable from a grantee.
+ *
+ * @param callCtx call context
+ * @param grantee resolved role, the grantee
+ * @param catalogPath path to that entity, cannot be null or empty unless securable is top-level
+ * @param securable securable entity, must have been resolved by the client. Can be the catalog
+ * itself.
+ * @param privilege privilege to revoke
+ * @return the result. Will return ENTITY_NOT_FOUND if the * specified role couldn't be found.
+ * Should be retried in that case. Will return GRANT_NOT_FOUND if the grant to revoke cannot
+ * be found
+ */
+ @NotNull
+ PrivilegeResult revokePrivilegeOnSecurableFromRole(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisEntityCore grantee,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityCore securable,
+ @NotNull PolarisPrivilege privilege);
+
+ /** Result of a load grants call */
+ class LoadGrantsResult extends BaseResult {
+ // true if success. If false, the caller should retry because of some concurrent change
+ private final int grantsVersion;
+
+ // null if not success. Else set of grants records on a securable or to a grantee
+ private final List grantRecords;
+
+ // null if not success. Else, for each grant record, list of securable or grantee entities
+ private final List entities;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public LoadGrantsResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.grantsVersion = 0;
+ this.grantRecords = null;
+ this.entities = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param grantsVersion version of the grants
+ * @param grantRecords set of grant records
+ */
+ public LoadGrantsResult(
+ int grantsVersion,
+ @NotNull List grantRecords,
+ List entities) {
+ super(ReturnStatus.SUCCESS);
+ this.grantsVersion = grantsVersion;
+ this.grantRecords = grantRecords;
+ this.entities = entities;
+ }
+
+ @JsonCreator
+ private LoadGrantsResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") String extraInformation,
+ @JsonProperty("grantsVersion") int grantsVersion,
+ @JsonProperty("grantRecords") List grantRecords,
+ @JsonProperty("entities") List entities) {
+ super(returnStatus, extraInformation);
+ this.grantsVersion = grantsVersion;
+ this.grantRecords = grantRecords;
+ // old GS code might not serialize this argument
+ this.entities = entities;
+ }
+
+ public int getGrantsVersion() {
+ return grantsVersion;
+ }
+
+ public List getGrantRecords() {
+ return grantRecords;
+ }
+
+ public List getEntities() {
+ return entities;
+ }
+
+ @JsonIgnore
+ public Map getEntitiesAsMap() {
+ return (this.getEntities() == null)
+ ? null
+ : this.getEntities().stream()
+ .collect(Collectors.toMap(PolarisBaseEntity::getId, entity -> entity));
+ }
+
+ @Override
+ public String toString() {
+ return "LoadGrantsResult{"
+ + "grantsVersion="
+ + grantsVersion
+ + ", grantRecords="
+ + grantRecords
+ + ", entities="
+ + entities
+ + ", returnStatus="
+ + getReturnStatus()
+ + '}';
+ }
+ }
+
+ /**
+ * This method should be used by the Polaris app to cache all grant records on a securable.
+ *
+ * @param callCtx call context
+ * @param securableCatalogId id of the catalog this securable belongs to
+ * @param securableId id of the securable
+ * @return the list of grants and the version of the grant records. We will return
+ * ENTITY_NOT_FOUND if the securable cannot be found
+ */
+ @NotNull
+ LoadGrantsResult loadGrantsOnSecurable(
+ @NotNull PolarisCallContext callCtx, long securableCatalogId, long securableId);
+
+ /**
+ * This method should be used by the Polaris app to load all grants made to a grantee, either a
+ * role or a principal.
+ *
+ * @param callCtx call context
+ * @param granteeCatalogId id of the catalog this grantee belongs to
+ * @param granteeId id of the grantee
+ * @return the list of grants and the version of the grant records. We will return NULL if the
+ * grantee does not exist
+ */
+ @NotNull
+ LoadGrantsResult loadGrantsToGrantee(
+ PolarisCallContext callCtx, long granteeCatalogId, long granteeId);
+
+ /** Result of a loadEntitiesChangeTracking call */
+ class ChangeTrackingResult extends BaseResult {
+
+ // null if not success. Else, will be null if the grant to revoke was not found
+ private final List changeTrackingVersions;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public ChangeTrackingResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.changeTrackingVersions = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param changeTrackingVersions change tracking versions
+ */
+ public ChangeTrackingResult(
+ @NotNull List changeTrackingVersions) {
+ super(ReturnStatus.SUCCESS);
+ this.changeTrackingVersions = changeTrackingVersions;
+ }
+
+ @JsonCreator
+ private ChangeTrackingResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") String extraInformation,
+ @JsonProperty("changeTrackingVersions")
+ List changeTrackingVersions) {
+ super(returnStatus, extraInformation);
+ this.changeTrackingVersions = changeTrackingVersions;
+ }
+
+ public List getChangeTrackingVersions() {
+ return changeTrackingVersions;
+ }
+ }
+
+ /**
+ * Load change tracking information for a set of entities in one single shot and return for each
+ * the version for the entity itself and the version associated to its grant records.
+ *
+ * @param callCtx call context
+ * @param entityIds list of catalog/entity pair ids for which we need to efficiently load the
+ * version information, both entity version and grant records version.
+ * @return a list of version tracking information. Order in that returned list is the same as the
+ * input list. Some elements might be NULL if the entity has been purged. Not expected to fail
+ */
+ @NotNull
+ ChangeTrackingResult loadEntitiesChangeTracking(
+ @NotNull PolarisCallContext callCtx, @NotNull List entityIds);
+
+ /**
+ * Load the entity from backend store. Will return NULL if the entity does not exist, i.e. has
+ * been purged. The entity being loaded might have been dropped
+ *
+ * @param callCtx call context
+ * @param entityCatalogId id of the catalog for that entity
+ * @param entityId the id of the entity to load
+ */
+ @NotNull
+ EntityResult loadEntity(@NotNull PolarisCallContext callCtx, long entityCatalogId, long entityId);
+
+ /**
+ * Fetch a list of tasks to be completed. Tasks
+ *
+ * @param callCtx call context
+ * @param executorId executor id
+ * @param limit limit
+ * @return list of tasks to be completed
+ */
+ @NotNull
+ EntitiesResult loadTasks(@NotNull PolarisCallContext callCtx, String executorId, int limit);
+
+ /** Result of a getSubscopedCredsForEntity() call */
+ class ScopedCredentialsResult extends BaseResult {
+
+ // null if not success. Else, set of name/value pairs for the credentials
+ private final EnumMap credentials;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public ScopedCredentialsResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.credentials = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param credentials credentials
+ */
+ public ScopedCredentialsResult(
+ @NotNull EnumMap credentials) {
+ super(ReturnStatus.SUCCESS);
+ this.credentials = credentials;
+ }
+
+ @JsonCreator
+ private ScopedCredentialsResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") String extraInformation,
+ @JsonProperty("credentials") Map credentials) {
+ super(returnStatus, extraInformation);
+ this.credentials = new EnumMap<>(PolarisCredentialProperty.class);
+ if (credentials != null) {
+ credentials.forEach(
+ (k, v) -> this.credentials.put(PolarisCredentialProperty.valueOf(k), v));
+ }
+ }
+
+ public EnumMap getCredentials() {
+ return credentials;
+ }
+ }
+
+ /**
+ * Get a sub-scoped credentials for an entity against the provided allowed read and write
+ * locations.
+ *
+ * @param callCtx the polaris call context
+ * @param catalogId the catalog id
+ * @param entityId the entity id
+ * @param allowListOperation whether to allow LIST operation on the allowedReadLocations and
+ * allowedWriteLocations
+ * @param allowedReadLocations a set of allowed to read locations
+ * @param allowedWriteLocations a set of allowed to write locations
+ * @return an enum map containing the scoped credentials
+ */
+ @NotNull
+ ScopedCredentialsResult getSubscopedCredsForEntity(
+ @NotNull PolarisCallContext callCtx,
+ long catalogId,
+ long entityId,
+ boolean allowListOperation,
+ @NotNull Set allowedReadLocations,
+ @NotNull Set allowedWriteLocations);
+
+ /** Result of a validateAccessToLocations() call */
+ class ValidateAccessResult extends BaseResult {
+
+ // null if not success. Else, set of location/validationResult pairs for each location in the
+ // set
+ private final Map validateResult;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public ValidateAccessResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.validateResult = null;
+ }
+
+ /**
+ * Constructor for success
+ *
+ * @param validateResult validate result
+ */
+ public ValidateAccessResult(@NotNull Map validateResult) {
+ super(ReturnStatus.SUCCESS);
+ this.validateResult = validateResult;
+ }
+
+ @JsonCreator
+ private ValidateAccessResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") String extraInformation,
+ @JsonProperty("validateResult") Map validateResult) {
+ super(returnStatus, extraInformation);
+ this.validateResult = validateResult;
+ }
+
+ public Map getValidateResult() {
+ return this.validateResult;
+ }
+ }
+
+ /**
+ * Validate whether the entity has access to the locations with the provided target operations
+ *
+ * @param callCtx the polaris call context
+ * @param catalogId the catalog id
+ * @param entityId the entity id
+ * @param actions a set of operation actions: READ/WRITE/LIST/DELETE/ALL
+ * @param locations a set of locations to verify
+ * @return a Map of , a validate result value looks like this
+ *
+ * {
+ * "status" : "failure",
+ * "actions" : {
+ * "READ" : {
+ * "message" : "The specified file was not found",
+ * "status" : "failure"
+ * },
+ * "DELETE" : {
+ * "message" : "One or more objects could not be deleted (Status Code: 200; Error Code: null)",
+ * "status" : "failure"
+ * },
+ * "LIST" : {
+ * "status" : "success"
+ * },
+ * "WRITE" : {
+ * "message" : "Access Denied (Status Code: 403; Error Code: AccessDenied)",
+ * "status" : "failure"
+ * }
+ * },
+ * "message" : "Some of the integration checks failed. Check the Snowflake documentation for more information."
+ * }
+ *
+ */
+ @NotNull
+ ValidateAccessResult validateAccessToLocations(
+ @NotNull PolarisCallContext callCtx,
+ long catalogId,
+ long entityId,
+ @NotNull Set actions,
+ @NotNull Set locations);
+
+ /**
+ * Represents an entry in the cache. If we refresh a cached entry, we will only refresh the
+ * information which have changed, based on the version of the entity
+ */
+ class CachedEntryResult extends BaseResult {
+
+ // the entity itself if it was loaded
+ private final @Nullable PolarisBaseEntity entity;
+
+ // version for the grant records, in case the entity was not loaded
+ private final int grantRecordsVersion;
+
+ private final @Nullable List entityGrantRecords;
+
+ /**
+ * Constructor for an error
+ *
+ * @param errorCode error code, cannot be SUCCESS
+ * @param extraInformation extra information
+ */
+ public CachedEntryResult(
+ @NotNull PolarisMetaStoreManager.ReturnStatus errorCode,
+ @Nullable String extraInformation) {
+ super(errorCode, extraInformation);
+ this.entity = null;
+ this.entityGrantRecords = null;
+ this.grantRecordsVersion = 0;
+ }
+
+ /**
+ * Constructor with success
+ *
+ * @param entity the entity for that cached entry
+ * @param grantRecordsVersion the version of the grant records
+ * @param entityGrantRecords the list of grant records
+ */
+ public CachedEntryResult(
+ @Nullable PolarisBaseEntity entity,
+ int grantRecordsVersion,
+ @Nullable List entityGrantRecords) {
+ super(ReturnStatus.SUCCESS);
+ this.entity = entity;
+ this.entityGrantRecords = entityGrantRecords;
+ this.grantRecordsVersion = grantRecordsVersion;
+ }
+
+ @JsonCreator
+ public CachedEntryResult(
+ @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus,
+ @JsonProperty("extraInformation") String extraInformation,
+ @Nullable @JsonProperty("entity") PolarisBaseEntity entity,
+ @JsonProperty("grantRecordsVersion") int grantRecordsVersion,
+ @Nullable @JsonProperty("entityGrantRecords") List entityGrantRecords) {
+ super(returnStatus, extraInformation);
+ this.entity = entity;
+ this.entityGrantRecords = entityGrantRecords;
+ this.grantRecordsVersion = grantRecordsVersion;
+ }
+
+ public @Nullable PolarisBaseEntity getEntity() {
+ return entity;
+ }
+
+ public int getGrantRecordsVersion() {
+ return grantRecordsVersion;
+ }
+
+ public @Nullable List getEntityGrantRecords() {
+ return entityGrantRecords;
+ }
+ }
+
+ /**
+ * Load a cached entry, i.e. an entity definition and associated grant records, from the backend
+ * store. The entity is identified by its id (entity catalog id and id).
+ *
+ * For entities that can be grantees, the associated grant records will include both the grant
+ * records for this entity as a grantee and for this entity as a securable.
+ *
+ * @param callCtx call context
+ * @param entityCatalogId id of the catalog for that entity
+ * @param entityId id of the entity
+ * @return cached entry for this entity. Status will be ENTITY_NOT_FOUND if the entity was not
+ * found
+ */
+ @NotNull
+ PolarisMetaStoreManager.CachedEntryResult loadCachedEntryById(
+ @NotNull PolarisCallContext callCtx, long entityCatalogId, long entityId);
+
+ /**
+ * Load a cached entry, i.e. an entity definition and associated grant records, from the backend
+ * store. The entity is identified by its name. Will return NULL if the entity does not exist,
+ * i.e. has been purged or dropped.
+ *
+ *
For entities that can be grantees, the associated grant records will include both the grant
+ * records for this entity as a grantee and for this entity as a securable.
+ *
+ * @param callCtx call context
+ * @param entityCatalogId id of the catalog for that entity
+ * @param parentId the id of the parent of that entity
+ * @param entityType the type of this entity
+ * @param entityName the name of this entity
+ * @return cached entry for this entity. Status will be ENTITY_NOT_FOUND if the entity was not
+ * found
+ */
+ @NotNull
+ PolarisMetaStoreManager.CachedEntryResult loadCachedEntryByName(
+ @NotNull PolarisCallContext callCtx,
+ long entityCatalogId,
+ long parentId,
+ @NotNull PolarisEntityType entityType,
+ @NotNull String entityName);
+
+ /**
+ * Refresh a cached entity from the backend store. Will return NULL if the entity does not exist,
+ * i.e. has been purged or dropped. Else, will determine what has changed based on the version
+ * information sent by the caller and will return only what has changed.
+ *
+ *
For entities that can be grantees, the associated grant records will include both the grant
+ * records for this entity as a grantee and for this entity as a securable.
+ *
+ * @param callCtx call context
+ * @param entityType type of the entity whose cached entry we are refreshing
+ * @param entityCatalogId id of the catalog for that entity
+ * @param entityId the id of the entity to load
+ * @return cached entry for this entity. Status will be ENTITY_NOT_FOUND if the entity was not *
+ * found
+ */
+ @NotNull
+ PolarisMetaStoreManager.CachedEntryResult refreshCachedEntity(
+ @NotNull PolarisCallContext callCtx,
+ int entityVersion,
+ int entityGrantRecordsVersion,
+ @NotNull PolarisEntityType entityType,
+ long entityCatalogId,
+ long entityId);
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManagerImpl.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManagerImpl.java
new file mode 100644
index 0000000000..710fbed1d3
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManagerImpl.java
@@ -0,0 +1,2413 @@
+/*
+ * Copyright (c) 2024 Snowflake Computing Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.polaris.core.persistence;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import io.polaris.core.PolarisCallContext;
+import io.polaris.core.entity.AsyncTaskType;
+import io.polaris.core.entity.PolarisBaseEntity;
+import io.polaris.core.entity.PolarisChangeTrackingVersions;
+import io.polaris.core.entity.PolarisEntitiesActiveKey;
+import io.polaris.core.entity.PolarisEntity;
+import io.polaris.core.entity.PolarisEntityActiveRecord;
+import io.polaris.core.entity.PolarisEntityConstants;
+import io.polaris.core.entity.PolarisEntityCore;
+import io.polaris.core.entity.PolarisEntityId;
+import io.polaris.core.entity.PolarisEntitySubType;
+import io.polaris.core.entity.PolarisEntityType;
+import io.polaris.core.entity.PolarisGrantRecord;
+import io.polaris.core.entity.PolarisPrincipalSecrets;
+import io.polaris.core.entity.PolarisPrivilege;
+import io.polaris.core.entity.PolarisTaskConstants;
+import io.polaris.core.storage.PolarisCredentialProperty;
+import io.polaris.core.storage.PolarisStorageActions;
+import io.polaris.core.storage.PolarisStorageConfigurationInfo;
+import io.polaris.core.storage.PolarisStorageIntegration;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Default implementation of the Polaris Meta Store Manager. Uses the underlying meta store to store
+ * and retrieve all Polaris metadata
+ */
+@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
+public class PolarisMetaStoreManagerImpl implements PolarisMetaStoreManager {
+
+ /** mapper, allows to serialize/deserialize properties to/from JSON */
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ /** use synchronous drop for entities */
+ private static final boolean USE_SYNCHRONOUS_DROP = true;
+
+ /**
+ * Lookup an entity by its name
+ *
+ * @param callCtx call context
+ * @param ms meta store
+ * @param entityActiveKey lookup key
+ * @return the entity if it exists, null otherwise
+ */
+ private @Nullable PolarisBaseEntity lookupEntityByName(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @NotNull PolarisEntitiesActiveKey entityActiveKey) {
+ // ensure that the entity exists
+ PolarisEntityActiveRecord entityActiveRecord = ms.lookupEntityActive(callCtx, entityActiveKey);
+
+ // if not found, return null
+ if (entityActiveRecord == null) {
+ return null;
+ }
+
+ // lookup the entity, should be there
+ PolarisBaseEntity entity =
+ ms.lookupEntity(callCtx, entityActiveRecord.getCatalogId(), entityActiveRecord.getId());
+ callCtx
+ .getDiagServices()
+ .checkNotNull(
+ entity, "unexpected_not_found_entity", "entityActiveRecord={}", entityActiveRecord);
+
+ // return it now
+ return entity;
+ }
+
+ /**
+ * Write this entity to the meta store.
+ *
+ * @param callCtx call context
+ * @param ms meta store in read/write mode
+ * @param entity entity to persist
+ * @param writeToActive if true, write it to active
+ */
+ private void writeEntity(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @NotNull PolarisBaseEntity entity,
+ boolean writeToActive) {
+ ms.writeToEntities(callCtx, entity);
+ ms.writeToEntitiesChangeTracking(callCtx, entity);
+
+ if (writeToActive) {
+ ms.writeToEntitiesActive(callCtx, entity);
+ }
+ }
+
+ /**
+ * Persist the specified new entity. Persist will write this entity in the ENTITIES, in the
+ * ENTITIES_ACTIVE and finally in the ENTITIES_CHANGE_TRACKING tables
+ *
+ * @param callCtx call context
+ * @param ms meta store in read/write mode
+ * @param entity entity we need a DPO for
+ */
+ private void persistNewEntity(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @NotNull PolarisBaseEntity entity) {
+
+ // validate the entity type and subtype
+ callCtx.getDiagServices().checkNotNull(entity, "unexpected_null_entity");
+ callCtx
+ .getDiagServices()
+ .checkNotNull(entity.getName(), "unexpected_null_name", "entity={}", entity);
+ PolarisEntityType type = PolarisEntityType.fromCode(entity.getTypeCode());
+ callCtx.getDiagServices().checkNotNull(type, "unknown_type", "entity={}", entity);
+ PolarisEntitySubType subType = PolarisEntitySubType.fromCode(entity.getSubTypeCode());
+ callCtx.getDiagServices().checkNotNull(subType, "unexpected_null_subType", "entity={}", entity);
+ callCtx
+ .getDiagServices()
+ .check(
+ subType.getParentType() == null || subType.getParentType() == type,
+ "invalid_subtype",
+ "type={} subType={}",
+ type,
+ subType);
+
+ // if top-level entity, its parent should be the account
+ callCtx
+ .getDiagServices()
+ .check(
+ !type.isTopLevel() || entity.getParentId() == PolarisEntityConstants.getRootEntityId(),
+ "top_level_parent_should_be_account",
+ "entity={}",
+ entity);
+
+ // id should not be null
+ callCtx
+ .getDiagServices()
+ .check(
+ entity.getId() != 0 || type == PolarisEntityType.ROOT,
+ "id_not_set",
+ "entity={}",
+ entity);
+
+ // creation timestamp must be filled
+ callCtx.getDiagServices().check(entity.getCreateTimestamp() != 0, "null_create_timestamp");
+
+ // this is the first change
+ entity.setLastUpdateTimestamp(entity.getCreateTimestamp());
+
+ // set all other timestamps to 0
+ entity.setDropTimestamp(0);
+ entity.setPurgeTimestamp(0);
+ entity.setToPurgeTimestamp(0);
+
+ // write it
+ this.writeEntity(callCtx, ms, entity, true);
+ }
+
+ /**
+ * Persist the specified entity after it has been changed. We will update the last changed time,
+ * increment the entity version and persist it back to the ENTITIES and ENTITIES_CHANGE_TRACKING
+ * tables
+ *
+ * @param callCtx call context
+ * @param ms meta store
+ * @param entity the entity which has been changed
+ * @return the entity with its version and lastUpdateTimestamp updated
+ */
+ private @NotNull PolarisBaseEntity persistEntityAfterChange(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @NotNull PolarisBaseEntity entity) {
+
+ // validate the entity type and subtype
+ callCtx.getDiagServices().checkNotNull(entity, "unexpected_null_entity");
+ callCtx
+ .getDiagServices()
+ .checkNotNull(entity.getName(), "unexpected_null_name", "entity={}", entity);
+ PolarisEntityType type = entity.getType();
+ callCtx.getDiagServices().checkNotNull(type, "unexpected_null_type", "entity={}", entity);
+ PolarisEntitySubType subType = entity.getSubType();
+ callCtx.getDiagServices().checkNotNull(subType, "unexpected_null_subType", "entity={}", entity);
+ callCtx
+ .getDiagServices()
+ .check(
+ subType.getParentType() == null || subType.getParentType() == type,
+ "invalid_subtype",
+ "type={} subType={} entity={}",
+ type,
+ subType,
+ entity);
+
+ // entity should not have been dropped
+ callCtx
+ .getDiagServices()
+ .check(entity.getDropTimestamp() == 0, "entity_dropped", "entity={}", entity);
+
+ // creation timestamp must be filled
+ long createTimestamp = entity.getCreateTimestamp();
+ callCtx
+ .getDiagServices()
+ .check(createTimestamp != 0, "null_create_timestamp", "entity={}", entity);
+
+ // ensure time is not moving backward...
+ long now = System.currentTimeMillis();
+ if (now < entity.getCreateTimestamp()) {
+ now = entity.getCreateTimestamp() + 1;
+ }
+
+ // update last update timestamp and increment entity version
+ entity.setLastUpdateTimestamp(now);
+ entity.setEntityVersion(entity.getEntityVersion() + 1);
+
+ // persist it to the various slices
+ this.writeEntity(callCtx, ms, entity, false);
+
+ // return it
+ return entity;
+ }
+
+ /**
+ * Drop this entity. This will:
+ *
+ *
+ * - validate that the entity has not yet been dropped
+ * - error out if this entity is undroppable
+ * - if this is a catalog or a namespace, error out if the entity still has children
+ * - we will fully delete the entity from persistence store
+ *
+ *
+ * @param callCtx call context
+ * @param ms meta store
+ * @param entity the entity being dropped
+ */
+ private void dropEntity(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @NotNull PolarisBaseEntity entity) {
+
+ // validate the entity type and subtype
+ callCtx.getDiagServices().checkNotNull(entity, "unexpected_null_dpo");
+ callCtx.getDiagServices().checkNotNull(entity.getName(), "unexpected_null_name");
+
+ // creation timestamp must be filled
+ callCtx.getDiagServices().check(entity.getDropTimestamp() == 0, "already_dropped");
+
+ // delete it from active slice
+ ms.deleteFromEntitiesActive(callCtx, entity);
+
+ // for now drop all entities synchronously
+ if (USE_SYNCHRONOUS_DROP) {
+ // use synchronous drop
+
+ // delete ALL grant records to (if the entity is a grantee) and from that entity
+ final List grantsOnGrantee =
+ (entity.getType().isGrantee())
+ ? ms.loadAllGrantRecordsOnGrantee(callCtx, entity.getCatalogId(), entity.getId())
+ : List.of();
+ final List grantsOnSecurable =
+ ms.loadAllGrantRecordsOnSecurable(callCtx, entity.getCatalogId(), entity.getId());
+ ms.deleteAllEntityGrantRecords(callCtx, entity, grantsOnGrantee, grantsOnSecurable);
+
+ // Now determine the set of entities on the other side of the grants we just removed. Grants
+ // from/to these entities has been removed, hence we need to update the grant version of
+ // each entity. Collect the id of each.
+ Set entityIdsGrantChanged = new HashSet<>();
+ grantsOnGrantee.forEach(
+ gr ->
+ entityIdsGrantChanged.add(
+ new PolarisEntityId(gr.getSecurableCatalogId(), gr.getSecurableId())));
+ grantsOnSecurable.forEach(
+ gr ->
+ entityIdsGrantChanged.add(
+ new PolarisEntityId(gr.getGranteeCatalogId(), gr.getGranteeId())));
+
+ // Bump up the grant version of these entities
+ List entities =
+ ms.lookupEntities(callCtx, new ArrayList<>(entityIdsGrantChanged));
+ for (PolarisBaseEntity entityGrantChanged : entities) {
+ entityGrantChanged.setGrantRecordsVersion(entityGrantChanged.getGrantRecordsVersion() + 1);
+ ms.writeToEntities(callCtx, entityGrantChanged);
+ ms.writeToEntitiesChangeTracking(callCtx, entityGrantChanged);
+ }
+
+ // remove the entity being dropped now
+ ms.deleteFromEntities(callCtx, entity);
+ ms.deleteFromEntitiesChangeTracking(callCtx, entity);
+
+ // if it is a principal, we also need to drop the secrets
+ if (entity.getType() == PolarisEntityType.PRINCIPAL) {
+ // get internal properties
+ Map properties =
+ this.deserializeProperties(callCtx, entity.getInternalProperties());
+
+ // get client_id
+ String clientId = properties.get(PolarisEntityConstants.getClientIdPropertyName());
+
+ // delete it from the secret slice
+ ms.deletePrincipalSecrets(callCtx, clientId, entity.getId());
+ }
+ } else {
+
+ // update the entity to indicate it has been dropped
+ final long now = System.currentTimeMillis();
+ entity.setDropTimestamp(now);
+ entity.setLastUpdateTimestamp(now);
+
+ // schedule purge
+ entity.setToPurgeTimestamp(now + PolarisEntityConstants.getRetentionTimeInMs());
+
+ // increment version
+ entity.setEntityVersion(entity.getEntityVersion() + 1);
+
+ // write to the dropped slice and to purge slice
+ ms.writeToEntities(callCtx, entity);
+ ms.writeToEntitiesDropped(callCtx, entity);
+ ms.writeToEntitiesChangeTracking(callCtx, entity);
+ }
+ }
+
+ /**
+ * Create and persist a new grant record. This will at the same time invalidate the grant records
+ * of the grantee and the securable if the grantee is a catalog role
+ *
+ * @param callCtx call context
+ * @param ms meta store in read/write mode
+ * @param securable securable
+ * @param grantee grantee, either a catalog role, a principal role or a principal
+ * @param priv privilege
+ * @return new grant record which was created and persisted
+ */
+ private @NotNull PolarisGrantRecord persistNewGrantRecord(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @NotNull PolarisEntityCore securable,
+ @NotNull PolarisEntityCore grantee,
+ @NotNull PolarisPrivilege priv) {
+
+ // validate non null arguments
+ callCtx.getDiagServices().checkNotNull(securable, "unexpected_null_securable");
+ callCtx.getDiagServices().checkNotNull(grantee, "unexpected_null_grantee");
+ callCtx.getDiagServices().checkNotNull(priv, "unexpected_null_priv");
+
+ // ensure that this entity is indeed a grantee like entity
+ callCtx
+ .getDiagServices()
+ .check(grantee.getType().isGrantee(), "entity_must_be_grantee", "entity={}", grantee);
+
+ // create new grant record
+ PolarisGrantRecord grantRecord =
+ new PolarisGrantRecord(
+ securable.getCatalogId(),
+ securable.getId(),
+ grantee.getCatalogId(),
+ grantee.getId(),
+ priv.getCode());
+
+ // persist the new grant
+ ms.writeToGrantRecords(callCtx, grantRecord);
+
+ // load the grantee (either a catalog/principal role or a principal) and increment its grants
+ // version
+ PolarisBaseEntity granteeEntity =
+ ms.lookupEntity(callCtx, grantee.getCatalogId(), grantee.getId());
+ callCtx
+ .getDiagServices()
+ .checkNotNull(granteeEntity, "grantee_not_found", "grantee={}", grantee);
+
+ // grants have changed, we need to bump-up the grants version
+ granteeEntity.setGrantRecordsVersion(granteeEntity.getGrantRecordsVersion() + 1);
+ this.writeEntity(callCtx, ms, granteeEntity, false);
+
+ // we also need to invalidate the grants on that securable so that we can reload them.
+ // load the securable and increment its grants version
+ PolarisBaseEntity securableEntity =
+ ms.lookupEntity(callCtx, securable.getCatalogId(), securable.getId());
+ callCtx
+ .getDiagServices()
+ .checkNotNull(securableEntity, "securable_not_found", "securable={}", securable);
+
+ // grants have changed, we need to bump-up the grants version
+ securableEntity.setGrantRecordsVersion(securableEntity.getGrantRecordsVersion() + 1);
+ this.writeEntity(callCtx, ms, securableEntity, false);
+
+ // done, return the new grant record
+ return grantRecord;
+ }
+
+ /**
+ * Delete the specified grant record from the GRANT_RECORDS table. This will at the same time
+ * invalidate the grant records of the grantee and the securable if the grantee is a role
+ *
+ * @param callCtx call context
+ * @param ms meta store
+ * @param securable the securable entity
+ * @param grantee the grantee entity
+ * @param grantRecord the grant record to remove, which was read in the same transaction
+ */
+ private void revokeGrantRecord(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @NotNull PolarisEntityCore securable,
+ @NotNull PolarisEntityCore grantee,
+ @NotNull PolarisGrantRecord grantRecord) {
+
+ // validate securable
+ callCtx
+ .getDiagServices()
+ .check(
+ securable.getCatalogId() == grantRecord.getSecurableCatalogId()
+ && securable.getId() == grantRecord.getSecurableId(),
+ "securable_mismatch",
+ "securable={} grantRec={}",
+ securable,
+ grantRecord);
+
+ // validate grantee
+ callCtx
+ .getDiagServices()
+ .check(
+ grantee.getCatalogId() == grantRecord.getGranteeCatalogId()
+ && grantee.getId() == grantRecord.getGranteeId(),
+ "grantee_mismatch",
+ "grantee={} grantRec={}",
+ grantee,
+ grantRecord);
+
+ // ensure the grantee is really a grantee
+ callCtx
+ .getDiagServices()
+ .check(grantee.getType().isGrantee(), "not_a_grantee", "grantee={}", grantee);
+
+ // remove that grant
+ ms.deleteFromGrantRecords(callCtx, grantRecord);
+
+ // load the grantee and increment its grants version
+ PolarisBaseEntity refreshGrantee =
+ ms.lookupEntity(callCtx, grantee.getCatalogId(), grantee.getId());
+ callCtx
+ .getDiagServices()
+ .checkNotNull(
+ refreshGrantee, "missing_grantee", "grantRecord={} grantee={}", grantRecord, grantee);
+
+ // grants have changed, we need to bump-up the grants version
+ refreshGrantee.setGrantRecordsVersion(refreshGrantee.getGrantRecordsVersion() + 1);
+ this.writeEntity(callCtx, ms, refreshGrantee, false);
+
+ // we also need to invalidate the grants on that securable so that we can reload them.
+ // load the securable and increment its grants version
+ PolarisBaseEntity refreshSecurable =
+ ms.lookupEntity(callCtx, securable.getCatalogId(), securable.getId());
+ callCtx
+ .getDiagServices()
+ .checkNotNull(
+ refreshSecurable,
+ "missing_securable",
+ "grantRecord={} securable={}",
+ grantRecord,
+ securable);
+
+ // grants have changed, we need to bump-up the grants version
+ refreshSecurable.setGrantRecordsVersion(refreshSecurable.getGrantRecordsVersion() + 1);
+ this.writeEntity(callCtx, ms, refreshSecurable, false);
+ }
+
+ /**
+ * Create a new catalog. This not only creates the new catalog entity but also the initial admin
+ * role required to admin this catalog.
+ *
+ * @param callCtx call context
+ * @param ms meta store in read/write mode
+ * @param catalog the catalog entity to create
+ * @param integration the storage integration that should be attached to the catalog. If null, do
+ * nothing, otherwise persist the integration.
+ * @param principalRoles once the catalog has been created, list of principal roles to grant its
+ * catalog_admin role to. If no principal role is specified, we will grant the catalog_admin
+ * role of the newly created catalog to the service admin role.
+ * @return the catalog we just created and its associated admin catalog role or error if we failed
+ * to
+ */
+ private @NotNull CreateCatalogResult createCatalog(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @NotNull PolarisBaseEntity catalog,
+ @Nullable PolarisStorageIntegration> integration,
+ @NotNull List principalRoles) {
+ // validate input
+ callCtx.getDiagServices().checkNotNull(catalog, "unexpected_null_catalog");
+
+ // check if that catalog has already been created
+ PolarisBaseEntity refreshCatalog =
+ ms.lookupEntity(callCtx, catalog.getCatalogId(), catalog.getId());
+
+ // if found, probably a retry, simply return the previously created catalog
+ if (refreshCatalog != null) {
+ // if found, ensure it is indeed a catalog
+ callCtx
+ .getDiagServices()
+ .check(
+ refreshCatalog.getTypeCode() == PolarisEntityType.CATALOG.getCode(),
+ "not_a_catalog",
+ "catalog={}",
+ catalog);
+
+ // lookup catalog admin role, should exist
+ PolarisEntitiesActiveKey adminRoleKey =
+ new PolarisEntitiesActiveKey(
+ refreshCatalog.getId(),
+ refreshCatalog.getId(),
+ PolarisEntityType.CATALOG_ROLE.getCode(),
+ PolarisEntityConstants.getNameOfCatalogAdminRole());
+ PolarisBaseEntity catalogAdminRole = this.lookupEntityByName(callCtx, ms, adminRoleKey);
+
+ // if found, ensure not null
+ callCtx
+ .getDiagServices()
+ .checkNotNull(
+ catalogAdminRole, "catalog_admin_role_not_found", "catalog={}", refreshCatalog);
+
+ // done, return the existing catalog
+ return new CreateCatalogResult(refreshCatalog, catalogAdminRole);
+ }
+
+ // check that a catalog with the same name does not exist already
+ PolarisEntitiesActiveKey catalogNameKey =
+ new PolarisEntitiesActiveKey(
+ PolarisEntityConstants.getNullId(),
+ PolarisEntityConstants.getRootEntityId(),
+ PolarisEntityType.CATALOG.getCode(),
+ catalog.getName());
+ PolarisEntityActiveRecord otherCatalogRecord = ms.lookupEntityActive(callCtx, catalogNameKey);
+
+ // if it exists, this is an error, the client should retry
+ if (otherCatalogRecord != null) {
+ return new CreateCatalogResult(ReturnStatus.ENTITY_ALREADY_EXISTS, null);
+ }
+
+ ms.persistStorageIntegrationIfNeeded(callCtx, catalog, integration);
+
+ // now create and persist new catalog entity
+ this.persistNewEntity(callCtx, ms, catalog);
+
+ // create the catalog admin role for this new catalog
+ long adminRoleId = ms.generateNewId(callCtx);
+ PolarisBaseEntity adminRole =
+ new PolarisBaseEntity(
+ catalog.getId(),
+ adminRoleId,
+ PolarisEntityType.CATALOG_ROLE,
+ PolarisEntitySubType.NULL_SUBTYPE,
+ catalog.getId(),
+ PolarisEntityConstants.getNameOfCatalogAdminRole());
+ this.persistNewEntity(callCtx, ms, adminRole);
+
+ // grant the catalog admin role access-management on the catalog
+ this.persistNewGrantRecord(
+ callCtx, ms, catalog, adminRole, PolarisPrivilege.CATALOG_MANAGE_ACCESS);
+
+ // grant the catalog admin role metadata-management on the catalog; this one
+ // is revocable
+ this.persistNewGrantRecord(
+ callCtx, ms, catalog, adminRole, PolarisPrivilege.CATALOG_MANAGE_METADATA);
+
+ // immediately assign its catalog_admin role
+ if (principalRoles.isEmpty()) {
+ // lookup service admin role, should exist
+ PolarisEntitiesActiveKey serviceAdminRoleKey =
+ new PolarisEntitiesActiveKey(
+ PolarisEntityConstants.getNullId(),
+ PolarisEntityConstants.getRootEntityId(),
+ PolarisEntityType.PRINCIPAL_ROLE.getCode(),
+ PolarisEntityConstants.getNameOfPrincipalServiceAdminRole());
+ PolarisBaseEntity serviceAdminRole =
+ this.lookupEntityByName(callCtx, ms, serviceAdminRoleKey);
+ callCtx.getDiagServices().checkNotNull(serviceAdminRole, "missing_service_admin_role");
+ this.persistNewGrantRecord(
+ callCtx, ms, adminRole, serviceAdminRole, PolarisPrivilege.CATALOG_ROLE_USAGE);
+ } else {
+ // grant to each principal role usage on its catalog_admin role
+ for (PolarisEntityCore principalRole : principalRoles) {
+ // validate not null and really a principal role
+ callCtx.getDiagServices().checkNotNull(principalRole, "null principal role");
+ callCtx
+ .getDiagServices()
+ .check(
+ principalRole.getTypeCode() == PolarisEntityType.PRINCIPAL_ROLE.getCode(),
+ "not_principal_role",
+ "type={}",
+ principalRole.getType());
+
+ // grant usage on that catalog admin role to this principal
+ this.persistNewGrantRecord(
+ callCtx, ms, adminRole, principalRole, PolarisPrivilege.CATALOG_ROLE_USAGE);
+ }
+ }
+
+ // success, return the two entities
+ return new CreateCatalogResult(catalog, adminRole);
+ }
+
+ /**
+ * Bootstrap Polaris catalog service
+ *
+ * @param callCtx call context
+ * @param ms meta store in read/write mode
+ */
+ private void bootstrapPolarisService(
+ @NotNull PolarisCallContext callCtx, @NotNull PolarisMetaStoreSession ms) {
+
+ // cleanup everything, start from a blank slate
+ ms.deleteAll(callCtx);
+
+ // Create a root container entity that can represent the securable for any top-level grants.
+ PolarisBaseEntity rootContainer =
+ new PolarisBaseEntity(
+ PolarisEntityConstants.getNullId(),
+ PolarisEntityConstants.getRootEntityId(),
+ PolarisEntityType.ROOT,
+ PolarisEntitySubType.NULL_SUBTYPE,
+ PolarisEntityConstants.getRootEntityId(),
+ PolarisEntityConstants.getRootContainerName());
+ this.persistNewEntity(callCtx, ms, rootContainer);
+
+ // Now bootstrap the service by creating the root principal and the service_admin principal
+ // role. The principal role will be granted to that root principal and the root catalog admin
+ // of the root catalog will be granted to that principal role.
+ long rootPrincipalId = ms.generateNewId(callCtx);
+ PolarisBaseEntity rootPrincipal =
+ new PolarisBaseEntity(
+ PolarisEntityConstants.getNullId(),
+ rootPrincipalId,
+ PolarisEntityType.PRINCIPAL,
+ PolarisEntitySubType.NULL_SUBTYPE,
+ PolarisEntityConstants.getRootEntityId(),
+ PolarisEntityConstants.getRootPrincipalName());
+
+ // create this principal
+ this.createPrincipal(callCtx, ms, rootPrincipal);
+
+ // now create the account admin principal role
+ long serviceAdminPrincipalRoleId = ms.generateNewId(callCtx);
+ PolarisBaseEntity serviceAdminPrincipalRole =
+ new PolarisBaseEntity(
+ PolarisEntityConstants.getNullId(),
+ serviceAdminPrincipalRoleId,
+ PolarisEntityType.PRINCIPAL_ROLE,
+ PolarisEntitySubType.NULL_SUBTYPE,
+ PolarisEntityConstants.getRootEntityId(),
+ PolarisEntityConstants.getNameOfPrincipalServiceAdminRole());
+ this.persistNewEntity(callCtx, ms, serviceAdminPrincipalRole);
+
+ // we also need to grant usage on the account-admin principal to the principal
+ this.persistNewGrantRecord(
+ callCtx,
+ ms,
+ serviceAdminPrincipalRole,
+ rootPrincipal,
+ PolarisPrivilege.PRINCIPAL_ROLE_USAGE);
+
+ // grant SERVICE_MANAGE_ACCESS on the rootContainer to the serviceAdminPrincipalRole
+ this.persistNewGrantRecord(
+ callCtx,
+ ms,
+ rootContainer,
+ serviceAdminPrincipalRole,
+ PolarisPrivilege.SERVICE_MANAGE_ACCESS);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull BaseResult bootstrapPolarisService(@NotNull PolarisCallContext callCtx) {
+ // get meta store we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // run operation in a read/write transaction
+ ms.runActionInTransaction(callCtx, () -> this.bootstrapPolarisService(callCtx, ms));
+
+ // all good
+ return new BaseResult(ReturnStatus.SUCCESS);
+ }
+
+ /**
+ * See {@link #readEntityByName(PolarisCallContext, List, PolarisEntityType, PolarisEntitySubType,
+ * String)}
+ */
+ private @NotNull PolarisMetaStoreManager.EntityResult readEntityByName(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityType entityType,
+ @NotNull PolarisEntitySubType entitySubType,
+ @NotNull String name) {
+ // first resolve again the catalogPath to that entity
+ PolarisEntityResolver resolver = new PolarisEntityResolver(callCtx, ms, catalogPath);
+
+ // return if we failed to resolve
+ if (resolver.isFailure()) {
+ return new EntityResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null);
+ }
+
+ // now looking the entity by name
+ PolarisEntitiesActiveKey entityActiveKey =
+ new PolarisEntitiesActiveKey(
+ resolver.getCatalogIdOrNull(), resolver.getParentId(), entityType.getCode(), name);
+ PolarisBaseEntity entity = this.lookupEntityByName(callCtx, ms, entityActiveKey);
+
+ // if found, check if subType really matches
+ if (entity != null
+ && entitySubType != PolarisEntitySubType.ANY_SUBTYPE
+ && entity.getSubTypeCode() != entitySubType.getCode()) {
+ entity = null;
+ }
+
+ // success, return what we found
+ return (entity == null)
+ ? new EntityResult(ReturnStatus.ENTITY_NOT_FOUND, null)
+ : new EntityResult(entity);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull PolarisMetaStoreManager.EntityResult readEntityByName(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityType entityType,
+ @NotNull PolarisEntitySubType entitySubType,
+ @NotNull String name) {
+ // get meta store we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // run operation in a read/write transaction
+ return ms.runInReadTransaction(
+ callCtx, () -> readEntityByName(callCtx, ms, catalogPath, entityType, entitySubType, name));
+ }
+
+ /**
+ * See {@link #listEntities(PolarisCallContext, List, PolarisEntityType, PolarisEntitySubType)}
+ */
+ private @NotNull ListEntitiesResult listEntities(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityType entityType,
+ @NotNull PolarisEntitySubType entitySubType) {
+ // first resolve again the catalogPath to that entity
+ PolarisEntityResolver resolver = new PolarisEntityResolver(callCtx, ms, catalogPath);
+
+ // return if we failed to resolve
+ if (resolver.isFailure()) {
+ return new ListEntitiesResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null);
+ }
+
+ // return list of active entities
+ List toreturnList =
+ ms.listActiveEntities(
+ callCtx, resolver.getCatalogIdOrNull(), resolver.getParentId(), entityType);
+
+ // prune the returned list with only entities matching the entity subtype
+ if (entitySubType != PolarisEntitySubType.ANY_SUBTYPE) {
+ toreturnList =
+ toreturnList.stream()
+ .filter(rec -> rec.getSubTypeCode() == entitySubType.getCode())
+ .collect(Collectors.toList());
+ }
+
+ // done
+ return new ListEntitiesResult(toreturnList);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull ListEntitiesResult listEntities(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityType entityType,
+ @NotNull PolarisEntitySubType entitySubType) {
+ // get meta store we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // run operation in a read transaction
+ return ms.runInReadTransaction(
+ callCtx, () -> listEntities(callCtx, ms, catalogPath, entityType, entitySubType));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull GenerateEntityIdResult generateNewEntityId(@NotNull PolarisCallContext callCtx) {
+ // get meta store we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ return new GenerateEntityIdResult(ms.generateNewId(callCtx));
+ }
+
+ /**
+ * Given the internal property as a map of key/value pairs, serialize it to a String
+ *
+ * @param properties a map of key/value pairs
+ * @return a String, the JSON representation of the map
+ */
+ public String serializeProperties(PolarisCallContext callCtx, Map properties) {
+
+ String jsonString = null;
+ try {
+ // Deserialize the JSON string to a Map
+ jsonString = MAPPER.writeValueAsString(properties);
+ } catch (JsonProcessingException ex) {
+ callCtx.getDiagServices().fail("got_json_processing_exception", "ex={}", ex);
+ }
+
+ return jsonString;
+ }
+
+ /**
+ * Given the serialized properties, deserialize those to a Map
+ *
+ * @param properties a JSON string representing the set of properties
+ * @return a Map of string
+ */
+ public Map deserializeProperties(PolarisCallContext callCtx, String properties) {
+
+ Map retProperties = null;
+ try {
+ // Deserialize the JSON string to a Map
+ retProperties = MAPPER.readValue(properties, new TypeReference<>() {});
+ } catch (JsonMappingException ex) {
+ callCtx.getDiagServices().fail("got_json_mapping_exception", "ex={}", ex);
+ } catch (JsonProcessingException ex) {
+ callCtx.getDiagServices().fail("got_json_processing_exception", "ex={}", ex);
+ }
+
+ return retProperties;
+ }
+
+ /** {@link #createPrincipal(PolarisCallContext, PolarisBaseEntity)} */
+ private @NotNull CreatePrincipalResult createPrincipal(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @NotNull PolarisBaseEntity principal) {
+ // validate input
+ callCtx.getDiagServices().checkNotNull(principal, "unexpected_null_principal");
+
+ // check if that catalog has already been created
+ PolarisBaseEntity refreshPrincipal =
+ ms.lookupEntity(callCtx, principal.getCatalogId(), principal.getId());
+
+ // if found, probably a retry, simply return the previously created principal
+ if (refreshPrincipal != null) {
+ // if found, ensure it is indeed a principal
+ callCtx
+ .getDiagServices()
+ .check(
+ principal.getTypeCode() == PolarisEntityType.PRINCIPAL.getCode(),
+ "not_a_principal",
+ "principal={}",
+ principal);
+
+ // get internal properties
+ Map properties =
+ this.deserializeProperties(callCtx, refreshPrincipal.getInternalProperties());
+
+ // get client_id
+ String clientId = properties.get(PolarisEntityConstants.getClientIdPropertyName());
+
+ // should not be null
+ callCtx
+ .getDiagServices()
+ .checkNotNull(
+ clientId,
+ "null_client_id",
+ "properties={}",
+ refreshPrincipal.getInternalProperties());
+ // ensure non null and non empty
+ callCtx
+ .getDiagServices()
+ .check(
+ !clientId.isEmpty(),
+ "empty_client_id",
+ "properties={}",
+ refreshPrincipal.getInternalProperties());
+
+ // get the main and secondary secrets for that client
+ PolarisPrincipalSecrets principalSecrets = ms.loadPrincipalSecrets(callCtx, clientId);
+
+ // should not be null
+ callCtx
+ .getDiagServices()
+ .checkNotNull(
+ principalSecrets,
+ "missing_principal_secrets",
+ "clientId={} principal={}",
+ clientId,
+ refreshPrincipal);
+
+ // done, return the newly created principal
+ return new CreatePrincipalResult(refreshPrincipal, principalSecrets);
+ }
+
+ // check that a principal with the same name does not exist already
+ PolarisEntitiesActiveKey principalNameKey =
+ new PolarisEntitiesActiveKey(
+ PolarisEntityConstants.getNullId(),
+ PolarisEntityConstants.getRootEntityId(),
+ PolarisEntityType.PRINCIPAL.getCode(),
+ principal.getName());
+ PolarisEntityActiveRecord otherPrincipalRecord =
+ ms.lookupEntityActive(callCtx, principalNameKey);
+
+ // if it exists, this is an error, the client should retry
+ if (otherPrincipalRecord != null) {
+ return new CreatePrincipalResult(ReturnStatus.ENTITY_ALREADY_EXISTS, null);
+ }
+
+ // generate new secretes for this principal
+ PolarisPrincipalSecrets principalSecrets =
+ ms.generateNewPrincipalSecrets(callCtx, principal.getName(), principal.getId());
+
+ // generate properties
+ Map internalProperties = getInternalPropertyMap(callCtx, principal);
+ internalProperties.put(
+ PolarisEntityConstants.getClientIdPropertyName(), principalSecrets.getPrincipalClientId());
+
+ // remember client id
+ principal.setInternalProperties(this.serializeProperties(callCtx, internalProperties));
+
+ // now create and persist new catalog entity
+ this.persistNewEntity(callCtx, ms, principal);
+
+ // success, return the two entities
+ return new CreatePrincipalResult(principal, principalSecrets);
+ }
+
+ /** {@inheritDoc} */
+ public @NotNull CreatePrincipalResult createPrincipal(
+ @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity principal) {
+ // get metastore we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // need to run inside a read/write transaction
+ return ms.runInTransaction(callCtx, () -> this.createPrincipal(callCtx, ms, principal));
+ }
+
+ /** See {@link #loadPrincipalSecrets(PolarisCallContext, String)} */
+ private @Nullable PolarisPrincipalSecrets loadPrincipalSecrets(
+ @NotNull PolarisCallContext callCtx, PolarisMetaStoreSession ms, @NotNull String clientId) {
+ return ms.loadPrincipalSecrets(callCtx, clientId);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull PrincipalSecretsResult loadPrincipalSecrets(
+ @NotNull PolarisCallContext callCtx, @NotNull String clientId) {
+ // get metastore we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // need to run inside a read/write transaction
+ PolarisPrincipalSecrets secrets =
+ ms.runInTransaction(callCtx, () -> this.loadPrincipalSecrets(callCtx, ms, clientId));
+
+ return (secrets == null)
+ ? new PrincipalSecretsResult(ReturnStatus.ENTITY_NOT_FOUND, null)
+ : new PrincipalSecretsResult(secrets);
+ }
+
+ /** See {@link #} */
+ private @Nullable PolarisPrincipalSecrets rotatePrincipalSecrets(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @NotNull String clientId,
+ long principalId,
+ @NotNull String masterSecret,
+ boolean reset) {
+ // if not found, the principal must have been dropped
+ EntityResult loadEntityResult =
+ loadEntity(callCtx, ms, PolarisEntityConstants.getNullId(), principalId);
+ if (loadEntityResult.getReturnStatus() != ReturnStatus.SUCCESS) {
+ return null;
+ }
+
+ PolarisBaseEntity principal = loadEntityResult.getEntity();
+ Map internalProps =
+ PolarisObjectMapperUtil.deserializeProperties(
+ callCtx,
+ principal.getInternalProperties() == null ? "{}" : principal.getInternalProperties());
+
+ boolean doReset =
+ reset
+ || internalProps.get(
+ PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)
+ != null;
+ PolarisPrincipalSecrets secrets =
+ ms.rotatePrincipalSecrets(callCtx, clientId, principalId, masterSecret, doReset);
+
+ if (reset
+ && !internalProps.containsKey(
+ PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)) {
+ internalProps.put(
+ PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE, "true");
+ principal.setInternalProperties(
+ PolarisObjectMapperUtil.serializeProperties(callCtx, internalProps));
+ principal.setEntityVersion(principal.getEntityVersion() + 1);
+ writeEntity(callCtx, ms, principal, true);
+ } else if (internalProps.containsKey(
+ PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)) {
+ internalProps.remove(PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE);
+ principal.setInternalProperties(
+ PolarisObjectMapperUtil.serializeProperties(callCtx, internalProps));
+ principal.setEntityVersion(principal.getEntityVersion() + 1);
+ writeEntity(callCtx, ms, principal, true);
+ }
+ return secrets;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull PrincipalSecretsResult rotatePrincipalSecrets(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull String clientId,
+ long principalId,
+ @NotNull String mainSecret,
+ boolean reset) {
+ // get metastore we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // need to run inside a read/write transaction
+ PolarisPrincipalSecrets secrets =
+ ms.runInTransaction(
+ callCtx,
+ () ->
+ this.rotatePrincipalSecrets(callCtx, ms, clientId, principalId, mainSecret, reset));
+
+ return (secrets == null)
+ ? new PrincipalSecretsResult(ReturnStatus.ENTITY_NOT_FOUND, null)
+ : new PrincipalSecretsResult(secrets);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull CreateCatalogResult createCatalog(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisBaseEntity catalog,
+ @NotNull List principalRoles) {
+ // get metastore we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ Map internalProp = getInternalPropertyMap(callCtx, catalog);
+ String integrationIdentifierOrId =
+ internalProp.get(PolarisEntityConstants.getStorageIntegrationIdentifierPropertyName());
+ String storageConfigInfoStr =
+ internalProp.get(PolarisEntityConstants.getStorageConfigInfoPropertyName());
+ PolarisStorageIntegration> integration;
+ // storageConfigInfo's presence is needed to create a storage integration
+ // and the catalog should not have an internal property of storage identifier or id yet
+ if (storageConfigInfoStr != null && integrationIdentifierOrId == null) {
+ integration =
+ ms.createStorageIntegration(
+ callCtx,
+ catalog.getCatalogId(),
+ catalog.getId(),
+ PolarisStorageConfigurationInfo.deserialize(
+ callCtx.getDiagServices(), storageConfigInfoStr));
+ } else {
+ integration = null;
+ }
+ // need to run inside a read/write transaction
+ return ms.runInTransaction(
+ callCtx, () -> this.createCatalog(callCtx, ms, catalog, integration, principalRoles));
+ }
+
+ /** {@link #createEntityIfNotExists(PolarisCallContext, List, PolarisBaseEntity)} */
+ private @NotNull EntityResult createEntityIfNotExists(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable List catalogPath,
+ @NotNull PolarisBaseEntity entity) {
+
+ // entity cannot be null
+ callCtx.getDiagServices().checkNotNull(entity, "unexpected_null_entity");
+
+ // entity name must be specified
+ callCtx.getDiagServices().checkNotNull(entity.getName(), "unexpected_null_entity_name");
+
+ // first, check if the entity has already been created, in which case we will simply return it
+ PolarisBaseEntity entityFound = ms.lookupEntity(callCtx, entity.getCatalogId(), entity.getId());
+ if (entityFound != null) {
+ // probably the client retried, simply return it
+ return new EntityResult(entityFound);
+ }
+
+ // first resolve again the catalogPath
+ PolarisEntityResolver resolver = new PolarisEntityResolver(callCtx, ms, catalogPath);
+
+ // return if we failed to resolve
+ if (resolver.isFailure()) {
+ return new EntityResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null);
+ }
+
+ // check if an entity does not already exist with the same name. If true, this is an error
+ PolarisEntitiesActiveKey entityActiveKey =
+ new PolarisEntitiesActiveKey(
+ entity.getCatalogId(),
+ entity.getParentId(),
+ entity.getType().getCode(),
+ entity.getName());
+ PolarisEntityActiveRecord entityActiveRecord = ms.lookupEntityActive(callCtx, entityActiveKey);
+ if (entityActiveRecord != null) {
+ return new EntityResult(
+ ReturnStatus.ENTITY_ALREADY_EXISTS, entityActiveRecord.getSubTypeCode());
+ }
+
+ // persist that new entity
+ this.persistNewEntity(callCtx, ms, entity);
+
+ // done, return that newly created entity
+ return new EntityResult(entity);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull EntityResult createEntityIfNotExists(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisBaseEntity entity) {
+ // get metastore we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // need to run inside a read/write transaction
+ return ms.runInTransaction(
+ callCtx, () -> this.createEntityIfNotExists(callCtx, ms, catalogPath, entity));
+ }
+
+ @Override
+ public @NotNull EntitiesResult createEntitiesIfNotExist(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull List extends PolarisBaseEntity> entities) {
+ // get metastore we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // need to run inside a read/write transaction
+ return ms.runInTransaction(
+ callCtx,
+ () -> {
+ List createdEntities = new ArrayList<>(entities.size());
+ for (PolarisBaseEntity entity : entities) {
+ EntityResult entityCreateResult =
+ createEntityIfNotExists(callCtx, ms, catalogPath, entity);
+ // abort everything if error
+ if (entityCreateResult.getReturnStatus() != ReturnStatus.SUCCESS) {
+ ms.rollback();
+ return new EntitiesResult(
+ entityCreateResult.getReturnStatus(), entityCreateResult.getExtraInformation());
+ }
+ createdEntities.add(entityCreateResult.getEntity());
+ }
+ return new EntitiesResult(createdEntities);
+ });
+ }
+
+ /**
+ * See {@link #updateEntityPropertiesIfNotChanged(PolarisCallContext, List, PolarisBaseEntity)}
+ */
+ private @NotNull EntityResult updateEntityPropertiesIfNotChanged(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable List catalogPath,
+ @NotNull PolarisBaseEntity entity) {
+ // entity cannot be null
+ callCtx.getDiagServices().checkNotNull(entity, "unexpected_null_entity");
+
+ // re-resolve everything including that entity
+ PolarisEntityResolver resolver = new PolarisEntityResolver(callCtx, ms, catalogPath, entity);
+
+ // if resolution failed, return false
+ if (resolver.isFailure()) {
+ return new EntityResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null);
+ }
+
+ // lookup the entity, cannot be null
+ PolarisBaseEntity entityRefreshed =
+ ms.lookupEntity(callCtx, entity.getCatalogId(), entity.getId());
+ callCtx
+ .getDiagServices()
+ .checkNotNull(entityRefreshed, "unexpected_entity_not_found", "entity={}", entity);
+
+ // check that the version of the entity has not changed at all to avoid concurrent updates
+ if (entityRefreshed.getEntityVersion() != entity.getEntityVersion()) {
+ return new EntityResult(ReturnStatus.TARGET_ENTITY_CONCURRENTLY_MODIFIED, null);
+ }
+
+ // update the two properties
+ entityRefreshed.setInternalProperties(entity.getInternalProperties());
+ entityRefreshed.setProperties(entity.getProperties());
+
+ // persist this entity after changing it. This will update the version and update the last
+ // updated time. Because the entity version is changed, we will update the change tracking table
+ PolarisBaseEntity persistedEntity = this.persistEntityAfterChange(callCtx, ms, entityRefreshed);
+ return new EntityResult(persistedEntity);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull EntityResult updateEntityPropertiesIfNotChanged(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisBaseEntity entity) {
+ // get metastore we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // need to run inside a read/write transaction
+ return ms.runInTransaction(
+ callCtx, () -> this.updateEntityPropertiesIfNotChanged(callCtx, ms, catalogPath, entity));
+ }
+
+ /** See {@link #updateEntitiesPropertiesIfNotChanged(PolarisCallContext, List)} */
+ private @NotNull EntitiesResult updateEntitiesPropertiesIfNotChanged(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @NotNull List entities) {
+ // ensure that the entities list is not null
+ callCtx.getDiagServices().checkNotNull(entities, "unexpected_null_entities");
+
+ // list of all updated entities
+ List updatedEntities = new ArrayList<>(entities.size());
+
+ // iterate over the list and update each, one at a time
+ for (EntityWithPath entityWithPath : entities) {
+ // update that entity, abort if it fails
+ EntityResult updatedEntityResult =
+ this.updateEntityPropertiesIfNotChanged(
+ callCtx, ms, entityWithPath.getCatalogPath(), entityWithPath.getEntity());
+
+ // if failed, rollback and return the last error
+ if (updatedEntityResult.getReturnStatus() != ReturnStatus.SUCCESS) {
+ ms.rollback();
+ return new EntitiesResult(
+ updatedEntityResult.getReturnStatus(), updatedEntityResult.getExtraInformation());
+ }
+
+ // one more was updated
+ updatedEntities.add(updatedEntityResult.getEntity());
+ }
+
+ // good, all success
+ return new EntitiesResult(updatedEntities);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull EntitiesResult updateEntitiesPropertiesIfNotChanged(
+ @NotNull PolarisCallContext callCtx, @NotNull List entities) {
+ // get metastore we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // need to run inside a read/write transaction
+ return ms.runInTransaction(
+ callCtx, () -> this.updateEntitiesPropertiesIfNotChanged(callCtx, ms, entities));
+ }
+
+ /**
+ * See {@link PolarisMetaStoreManager#renameEntity(PolarisCallContext, List, PolarisEntityCore,
+ * List, PolarisEntity)}
+ */
+ private @NotNull EntityResult renameEntity(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityCore entityToRename,
+ @Nullable List newCatalogPath,
+ @NotNull PolarisBaseEntity renamedEntity) {
+
+ // entity and new name cannot be null
+ callCtx.getDiagServices().checkNotNull(entityToRename, "unexpected_null_entityToRename");
+ callCtx.getDiagServices().checkNotNull(renamedEntity, "unexpected_null_renamedEntity");
+
+ // if a new catalog path is specified (i.e. re-parent operation), a catalog path should be
+ // specified too
+ callCtx
+ .getDiagServices()
+ .check(
+ (newCatalogPath == null) || (catalogPath != null),
+ "newCatalogPath_specified_without_catalogPath");
+
+ // null is shorthand for saying the path isn't changing
+ if (newCatalogPath == null) {
+ newCatalogPath = catalogPath;
+ }
+
+ // re-resolve everything including that entity
+ PolarisEntityResolver resolver =
+ new PolarisEntityResolver(callCtx, ms, catalogPath, entityToRename);
+
+ // if resolution failed, return false
+ if (resolver.isFailure()) {
+ return new EntityResult(ReturnStatus.ENTITY_CANNOT_BE_RESOLVED, null);
+ }
+
+ // find the entity to rename
+ PolarisBaseEntity refreshEntityToRename =
+ ms.lookupEntity(callCtx, entityToRename.getCatalogId(), entityToRename.getId());
+
+ // if this entity was not found, return failure. Not expected here because it was
+ // resolved successfully (see above)
+ if (refreshEntityToRename == null) {
+ return new EntityResult(ReturnStatus.ENTITY_NOT_FOUND, null);
+ }
+
+ // check that the source entity has not changed since it was updated by the caller
+ if (refreshEntityToRename.getEntityVersion() != renamedEntity.getEntityVersion()) {
+ return new EntityResult(ReturnStatus.TARGET_ENTITY_CONCURRENTLY_MODIFIED, null);
+ }
+
+ // ensure it can be renamed
+ if (refreshEntityToRename.cannotBeDroppedOrRenamed()) {
+ return new EntityResult(ReturnStatus.ENTITY_CANNOT_BE_RENAMED, null);
+ }
+
+ // re-resolve the new catalog path if this entity is going to be moved
+ if (newCatalogPath != null) {
+ resolver = new PolarisEntityResolver(callCtx, ms, newCatalogPath);
+
+ // if resolution failed, return false
+ if (resolver.isFailure()) {
+ return new EntityResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null);
+ }
+ }
+
+ // ensure that nothing exists where we create that entity
+ PolarisEntitiesActiveKey entityActiveKey =
+ new PolarisEntitiesActiveKey(
+ resolver.getCatalogIdOrNull(),
+ resolver.getParentId(),
+ refreshEntityToRename.getTypeCode(),
+ renamedEntity.getName());
+ // if this entity already exists, this is an error
+ PolarisEntityActiveRecord entityActiveRecord = ms.lookupEntityActive(callCtx, entityActiveKey);
+ if (entityActiveRecord != null) {
+ return new EntityResult(
+ ReturnStatus.ENTITY_ALREADY_EXISTS, entityActiveRecord.getSubTypeCode());
+ }
+
+ // all good, delete the existing entity from the active slice
+ ms.deleteFromEntitiesActive(callCtx, refreshEntityToRename);
+
+ // change its name now
+ refreshEntityToRename.setName(renamedEntity.getName());
+ refreshEntityToRename.setProperties(renamedEntity.getProperties());
+ refreshEntityToRename.setInternalProperties(renamedEntity.getInternalProperties());
+
+ // re-parent if a new catalog path was specified
+ if (newCatalogPath != null) {
+ refreshEntityToRename.setParentId(resolver.getParentId());
+ }
+
+ // persist back to the active slice with its new name and parent
+ ms.writeToEntitiesActive(callCtx, refreshEntityToRename);
+
+ // persist the entity after change. This wil update the lastUpdateTimestamp and bump up the
+ // version
+ PolarisBaseEntity renamedEntityToReturn =
+ this.persistEntityAfterChange(callCtx, ms, refreshEntityToRename);
+ return new EntityResult(renamedEntityToReturn);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull EntityResult renameEntity(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityCore entityToRename,
+ @Nullable List newCatalogPath,
+ @NotNull PolarisEntity renamedEntity) {
+ // get metastore we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // need to run inside a read/write transaction
+ return ms.runInTransaction(
+ callCtx,
+ () ->
+ this.renameEntity(
+ callCtx, ms, catalogPath, entityToRename, newCatalogPath, renamedEntity));
+ }
+
+ /**
+ * See
+ *
+ * {@link #dropEntityIfExists(PolarisCallContext, List, PolarisEntityCore, Map, boolean)}
+ */
+ private @NotNull DropEntityResult dropEntityIfExists(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityCore entityToDrop,
+ @Nullable Map cleanupProperties,
+ boolean cleanup) {
+ // entity cannot be null
+ callCtx.getDiagServices().checkNotNull(entityToDrop, "unexpected_null_entity");
+
+ // re-resolve everything including that entity
+ PolarisEntityResolver resolver =
+ new PolarisEntityResolver(callCtx, ms, catalogPath, entityToDrop);
+
+ // if resolution failed, return false
+ if (resolver.isFailure()) {
+ return new DropEntityResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null);
+ }
+
+ // first find the entity to drop
+ PolarisBaseEntity refreshEntityToDrop =
+ ms.lookupEntity(callCtx, entityToDrop.getCatalogId(), entityToDrop.getId());
+
+ // if this entity was not found, return failure
+ if (refreshEntityToDrop == null) {
+ return new DropEntityResult(ReturnStatus.ENTITY_NOT_FOUND, null);
+ }
+
+ // ensure that this entity is droppable
+ if (refreshEntityToDrop.cannotBeDroppedOrRenamed()) {
+ return new DropEntityResult(ReturnStatus.ENTITY_UNDROPPABLE, null);
+ }
+
+ // check that the entity has children, in which case it is an error. This only applies to
+ // a namespaces or a catalog
+ if (refreshEntityToDrop.getType() == PolarisEntityType.CATALOG) {
+ // the id of the catalog
+ long catalogId = refreshEntityToDrop.getId();
+
+ // if not all namespaces are dropped, we cannot drop this catalog
+ if (ms.hasChildren(callCtx, PolarisEntityType.NAMESPACE, catalogId, catalogId)) {
+ return new DropEntityResult(ReturnStatus.NAMESPACE_NOT_EMPTY, null);
+ }
+
+ // get the list of catalog roles, at most 2
+ List catalogRoles =
+ ms.listActiveEntities(
+ callCtx,
+ catalogId,
+ catalogId,
+ PolarisEntityType.CATALOG_ROLE,
+ 2,
+ entity -> true,
+ Function.identity());
+
+ // if we have 2, we cannot drop the catalog. If only one left, better be the admin role
+ if (catalogRoles.size() > 1) {
+ return new DropEntityResult(ReturnStatus.CATALOG_NOT_EMPTY, null);
+ }
+
+ // if 1, drop the last catalog role. Should be the catalog admin role but don't validate this
+ if (!catalogRoles.isEmpty()) {
+ // drop the last catalog role in that catalog, should be the admin catalog role
+ this.dropEntity(callCtx, ms, catalogRoles.get(0));
+ }
+ } else if (refreshEntityToDrop.getType() == PolarisEntityType.NAMESPACE) {
+ if (ms.hasChildren(
+ callCtx, null, refreshEntityToDrop.getCatalogId(), refreshEntityToDrop.getId())) {
+ return new DropEntityResult(ReturnStatus.NAMESPACE_NOT_EMPTY, null);
+ }
+ }
+
+ // simply delete that entity. Will be removed from entities_active, added to the
+ // entities_dropped and its version will be changed.
+ this.dropEntity(callCtx, ms, refreshEntityToDrop);
+
+ // if cleanup, schedule a cleanup task for the entity. do this here, so that drop and scheduling
+ // the cleanup task is transactional. Otherwise, we'll be unable to schedule the cleanup task
+ // later
+ if (cleanup) {
+ PolarisBaseEntity taskEntity =
+ new PolarisEntity.Builder()
+ .setId(generateNewEntityId(callCtx).getId())
+ .setCatalogId(0L)
+ .setName("entityCleanup_" + entityToDrop.getId())
+ .setType(PolarisEntityType.TASK)
+ .setSubType(PolarisEntitySubType.NULL_SUBTYPE)
+ .setCreateTimestamp(callCtx.getClock().millis())
+ .build();
+
+ Map properties = new HashMap<>();
+ properties.put(
+ PolarisTaskConstants.TASK_TYPE,
+ String.valueOf(AsyncTaskType.ENTITY_CLEANUP_SCHEDULER.typeCode()));
+ properties.put("data", PolarisObjectMapperUtil.serialize(callCtx, refreshEntityToDrop));
+ taskEntity.setProperties(PolarisObjectMapperUtil.serializeProperties(callCtx, properties));
+ if (cleanupProperties != null) {
+ taskEntity.setInternalProperties(
+ PolarisObjectMapperUtil.serializeProperties(callCtx, cleanupProperties));
+ }
+ createEntityIfNotExists(callCtx, ms, null, taskEntity);
+ return new DropEntityResult(taskEntity.getId());
+ }
+
+ // done, return success
+ return new DropEntityResult();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public @NotNull DropEntityResult dropEntityIfExists(
+ @NotNull PolarisCallContext callCtx,
+ @Nullable List catalogPath,
+ @NotNull PolarisEntityCore entityToDrop,
+ @Nullable Map cleanupProperties,
+ boolean cleanup) {
+ // get metastore we should be using
+ PolarisMetaStoreSession ms = callCtx.getMetaStore();
+
+ // need to run inside a read/write transaction
+ return ms.runInTransaction(
+ callCtx,
+ () ->
+ this.dropEntityIfExists(
+ callCtx, ms, catalogPath, entityToDrop, cleanupProperties, cleanup));
+ }
+
+ /**
+ * Resolve the arguments of granting/revoking a usage grant between a role (catalog or principal
+ * role) and a grantee (either a principal role or a principal)
+ *
+ * @param callCtx call context
+ * @param ms meta store in read/write mode
+ * @param catalog if the role is a catalog role, the caller needs to pass-in the catalog entity
+ * which was used to resolve that role. Else null.
+ * @param role the role, either a catalog or principal role
+ * @param grantee the grantee
+ * @return resolver for the specified entities
+ */
+ private @NotNull PolarisEntityResolver resolveRoleToGranteeUsageGrant(
+ @NotNull PolarisCallContext callCtx,
+ @NotNull PolarisMetaStoreSession ms,
+ @Nullable PolarisEntityCore catalog,
+ @NotNull PolarisEntityCore role,
+ @NotNull PolarisEntityCore grantee) {
+
+ // validate the grantee input
+ callCtx.getDiagServices().checkNotNull(grantee, "unexpected_null_grantee");
+ callCtx
+ .getDiagServices()
+ .check(grantee.getType().isGrantee(), "not_a_grantee", "grantee={}", grantee);
+
+ // validate role
+ callCtx.getDiagServices().checkNotNull(role, "unexpected_null_role");
+
+ // role should be a catalog or a principal role
+ boolean isCatalogRole = role.getTypeCode() == PolarisEntityType.CATALOG_ROLE.getCode();
+ boolean isPrincipalRole = role.getTypeCode() == PolarisEntityType.PRINCIPAL_ROLE.getCode();
+ callCtx.getDiagServices().check(isCatalogRole || isPrincipalRole, "not_a_role");
+
+ // if the role is a catalog role, ensure a catalog is specified and
+ // vice-versa, catalog should be null if the role is a principal role
+ callCtx
+ .getDiagServices()
+ .check(
+ (catalog == null && isPrincipalRole) || (catalog != null && isCatalogRole),
+ "catalog_mismatch",
+ "catalog={} role={}",
+ catalog,
+ role);
+
+ // re-resolve now all these entities
+ List