yuqi1129 commented on code in PR #13699:
URL: https://github.com/apache/gravitino/pull/13699#discussion_r4219300214
##########
server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataIdConverter.java:
##########
@@ -95,14 +98,55 @@ public static Optional<Long> getID(MetadataObject
metadataObject, String metalak
return Optional.of(extractIdFromEntity(entity));
}
+ /**
+ * Normalizes a metadata object's name using the same catalog rules as ID
resolution.
+ *
+ * <p>Types without a catalog capability scope retain their names. Semantic
model leaves remain
+ * case sensitive, while column names and their table/schema parents follow
their own scopes.
+ *
+ * @param metadataObject the object whose name will be normalized
+ * @param metalake the metalake name
+ * @return the normalized object, suitable for name-to-ID cache keys
+ * @throws NotFoundException if the containing catalog does not exist
+ */
+ public static MetadataObject normalizeMetadataObject(
+ MetadataObject metadataObject, String metalake) {
+ Preconditions.checkArgument(metadataObject != null, "Metadata object
cannot be null");
+ Capability.Scope scope = METADATA_SCOPE_MAPPING.get(metadataObject.type());
+ if (scope == null) {
+ return metadataObject;
+ }
+ NameIdentifier ident = MetadataObjectUtil.toEntityIdent(metalake,
metadataObject);
+ NameIdentifier normalized =
+ normalizeIdentifier(ident, scope,
GravitinoEnv.getInstance().catalogManager());
+ if (normalized.equals(ident)) {
+ return metadataObject;
+ }
+ return NameIdentifierUtil.toMetadataObject(
+ normalized, MetadataObjectUtil.toEntityType(metadataObject));
+ }
+
@VisibleForTesting
static NameIdentifier normalizeCaseSensitive(
NameIdentifier ident, Capability.Scope scope, CatalogManager
catalogManager) {
+ return normalizeIdentifier(ident, scope, catalogManager);
+ }
+
+ private static NameIdentifier normalizeIdentifier(
+ NameIdentifier ident, Capability.Scope scope, CatalogManager
catalogManager) {
if (scope == null) {
return ident;
}
Capability capability = CapabilityHelpers.getCapability(ident,
catalogManager);
+ if (scope == Capability.Scope.COLUMN) {
+ NameIdentifier table =
+ CapabilityHelpers.applyCaseSensitive(
+ NameIdentifier.of(ident.namespace().levels()),
Capability.Scope.TABLE, capability);
+ return NameIdentifier.of(
+ Namespace.of(ObjectArrays.concat(table.namespace().levels(),
table.name())),
+ CapabilityHelpers.applyCaseSensitiveOnName(scope, ident.name(),
capability));
+ }
Review Comment:
Checked the actual overload:
CapabilityHelpers.applyCaseSensitive(NameIdentifier, Scope, Capability) exists.
It normalizes the namespace with SCHEMA scope and the identifier leaf with
TABLE scope, so passing the column's parent table does not apply TABLE rules to
the schema. No production behavior change was needed here. In 764d6b75d I added
a clarifying comment and the complementary mixed-scope test (case-sensitive
schema/column, case-insensitive table), alongside the existing case-insensitive
schema/column, case-sensitive table test. Both pass; mutating the parent scope
causes the new test to fail.
##########
server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizationLookups.java:
##########
@@ -74,34 +67,25 @@ public JcasbinAuthorizationLookups(
* Optional#empty()} when the metadata object does not exist so callers can
deny authorization.
* Missing metadata objects are never cached as negative results: a later
create for the same name
* can be observed without waiting for cache eviction. Existing objects are
invalidated by local
- * name-id mapping hooks and by the change-log poller on peer nodes.
+ * name-id mapping hooks and by the change-log poller on peer nodes. Both
cache tiers use names
+ * normalized by catalog capability; capability lookup therefore also occurs
on cache hits.
*/
public Optional<Long> resolveMetadataId(
MetadataObject metadataObject, String metalake,
AuthorizationRequestContext requestContext) {
try {
- MetadataObject cacheObject = metadataObject;
- // Hooks and change-log records use normalized parents. Cache the same
key for every
- // equivalent request spelling, while preserving the Gravitino-owned
model leaf.
- if (metadataObject.type() == MetadataObject.Type.SEMANTIC_MODEL) {
- NameIdentifier ident = MetadataObjectUtil.toEntityIdent(metalake,
metadataObject);
- Capability capability =
- CapabilityHelpers.getCapability(ident,
GravitinoEnv.getInstance().catalogManager());
- NameIdentifier normalized =
- NameIdentifier.of(
- CapabilityHelpers.applyCaseSensitive(
- ident.namespace(), Capability.Scope.SEMANTIC_MODEL,
capability),
- ident.name());
- cacheObject =
- NameIdentifierUtil.toMetadataObject(normalized,
Entity.EntityType.SEMANTIC_MODEL);
- }
+ // Use the same capability rules as ID resolution so hooks and peer
change-log replay
+ // evict every equivalent spelling from both cache tiers.
+ MetadataObject cacheObject =
+ MetadataIdConverter.normalizeMetadataObject(metadataObject,
metalake);
String cacheKey =
JcasbinAuthorizationCacheKeys.metadataIdCacheKey(metalake, cacheObject);
// Both cache tiers load atomically and forbid caching null, so a
missing object is signalled
// by throwing through the loaders and translated back to
Optional.empty() here. This caches
- // only positive results, never a negative one.
+ // only positive results, never a negative one. Load the same canonical
name as the key,
+ // rather than applying the request spelling again in the loader.
return Optional.of(
requestContext.computeMetadataIdIfAbsent(
cacheKey,
- k -> metadataIdCache.get(k, ignored ->
loadMetadataId(metadataObject, metalake))));
+ k -> metadataIdCache.get(k, ignored ->
loadMetadataId(cacheObject, metalake))));
Review Comment:
Addressed in 764d6b75d with request-local normalization memoization.
Repeated lookups of the same raw metadata name now reuse its canonical object
before consulting the ID caches. The key includes metalake, object type and raw
name. Fresh requests still resolve current catalog rules, and normalization
failures are not cached. No cross-request capability TTL cache is introduced.
Tests verify capability lookup counts on repeated/shared-cache hits,
fresh-request behavior, retry after failure and concurrent deduplication;
bypassing memoization fails the lookup-count assertion. Distinct raw names
still require their own normalization.
##########
server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinMetadataIdCacheNormalization.java:
##########
@@ -0,0 +1,454 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.gravitino.server.authorization.jcasbin;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Stream;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.HasIdentifier;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.authorization.AccessControlDispatcher;
+import org.apache.gravitino.authorization.AuthorizationRequestContext;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.GravitinoAuthorizer;
+import org.apache.gravitino.cache.CaffeineGravitinoCache;
+import org.apache.gravitino.catalog.CatalogDispatcher;
+import org.apache.gravitino.catalog.CatalogManager;
+import org.apache.gravitino.catalog.SchemaDispatcher;
+import org.apache.gravitino.catalog.TableDispatcher;
+import org.apache.gravitino.connector.BaseCatalog;
+import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.connector.capability.CapabilityResult;
+import org.apache.gravitino.exceptions.NoSuchCatalogException;
+import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.hook.SchemaHookDispatcher;
+import org.apache.gravitino.hook.TableHookDispatcher;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.server.authorization.MetadataIdConverter;
+import
org.apache.gravitino.storage.relational.EntityChangeLogNameIdentifierCodec;
+import org.apache.gravitino.storage.relational.po.auth.OwnerInfo;
+import org.apache.gravitino.storage.relational.po.cache.EntityChangeRecord;
+import org.apache.gravitino.storage.relational.po.cache.OperateType;
+import org.apache.gravitino.utils.EntityClassMapper;
+import org.apache.gravitino.utils.MetadataObjectUtil;
+import org.apache.gravitino.utils.ThrowableFunction;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.MockedStatic;
+
+/** Verifies capability-normalized authorization cache keys through local and
peer invalidation. */
+public class TestJcasbinMetadataIdCacheNormalization {
+ private static final String METALAKE = "metalake";
+ private static final Capability CASE_INSENSITIVE =
+ new Capability() {
+ @Override
+ public CapabilityResult caseSensitiveOnName(Scope scope) {
+ return CapabilityResult.unsupported("case insensitive");
+ }
+ };
+
+ private final Map<NameIdentifier, Entity> entities = new HashMap<>();
+ private CaffeineGravitinoCache<String, Long> metadataCache;
+ private CaffeineGravitinoCache<Long, Optional<OwnerInfo>> ownerCache;
+ private CatalogManager catalogs;
+ private BaseCatalog<?> catalog;
+ private EntityStore store;
+ private GravitinoEnv env;
+ private JcasbinAuthorizationLookups lookups;
+ private GravitinoAuthorizer authorizer;
+ private MockedStatic<GravitinoEnv> envMock;
+
+ @BeforeEach
+ void setup() throws Exception {
+ metadataCache = new CaffeineGravitinoCache<>(60_000L, 100L);
+ ownerCache = new CaffeineGravitinoCache<>(60_000L, 100L);
+ lookups = new JcasbinAuthorizationLookups(metadataCache, ownerCache);
+ JcasbinAuthorizer localAuthorizer = new JcasbinAuthorizer();
+ // The name-ID hook only needs the same cache owned by the initialized
authorizer.
+ FieldUtils.writeDeclaredField(localAuthorizer, "metadataIdCache",
metadataCache, true);
+ authorizer = localAuthorizer;
+ catalog = mock(BaseCatalog.class);
+ when(catalog.capability()).thenReturn(CASE_INSENSITIVE);
+ when(catalog.provider()).thenReturn("test");
+ when(catalog.type()).thenReturn(Catalog.Type.RELATIONAL);
+ catalogs = mock(CatalogManager.class);
+ doAnswer(
+ invocation -> {
+ ThrowableFunction<BaseCatalog<?>, Object> operation =
invocation.getArgument(1);
+ return operation.apply(catalog);
+ })
+ .when(catalogs)
+ .doWithCatalog(any(), any());
+ store = mock(EntityStore.class);
+ when(store.get(any(), any(), any()))
+ .thenAnswer(
+ invocation -> {
+ NameIdentifier ident = invocation.getArgument(0);
+ Entity entity = entities.get(ident);
+ if (entity == null) {
+ throw new NoSuchEntityException("Missing entity: %s", ident);
+ }
+ return entity;
+ });
+ env = mock(GravitinoEnv.class);
+ when(env.entityStore()).thenReturn(store);
+ when(env.catalogManager()).thenReturn(catalogs);
+ when(env.gravitinoAuthorizer()).thenReturn(authorizer);
+
when(env.internalAccessControlDispatcher()).thenReturn(mock(AccessControlDispatcher.class));
+ CatalogDispatcher catalogDispatcher = mock(CatalogDispatcher.class);
+ when(catalogDispatcher.loadCatalog(any())).thenReturn(catalog);
+ when(env.internalCatalogDispatcher()).thenReturn(catalogDispatcher);
+ SchemaDispatcher schemaDispatcher = mock(SchemaDispatcher.class);
+ when(schemaDispatcher.loadSchema(any())).thenReturn(mock(Schema.class));
+ when(env.internalSchemaDispatcher()).thenReturn(schemaDispatcher);
+ envMock = mockStatic(GravitinoEnv.class);
+ envMock.when(GravitinoEnv::getInstance).thenReturn(env);
+ }
+
+ @AfterEach
+ void cleanup() {
+ if (envMock != null) {
+ envMock.close();
+ }
+ metadataCache.close();
+ ownerCache.close();
+ }
+
+ @ParameterizedTest
+ @MethodSource("catalogScopedTypes")
+ void
testAllMappedTypesNormalizeNamesIncludingColumnParents(MetadataObject.Type
type) {
+ assertEquals(
+ object(type, false),
+ MetadataIdConverter.normalizeMetadataObject(object(type, true),
METALAKE));
+ }
+
+ @ParameterizedTest
+ @MethodSource("idLookupTypes")
+ void testLocalNotificationEvictsAliasesAfterNameReuse(MetadataObject.Type
type)
+ throws IOException {
+ MetadataObject alias = object(type, true);
+ MetadataObject normalized = object(type, false);
+ put(normalized, 100L);
+ assertAliasesShareBothCacheTiers(alias, normalized);
+ AuthorizationUtils.notifyEntityNameIdMappingChange(ident(normalized),
entityType(type));
+ assertEquals(0L, metadataCache.size());
+ entities.remove(ident(normalized));
+ assertEquals(Optional.empty(), resolve(alias));
+ put(normalized, 200L);
+ assertEquals(Optional.of(200L), resolve(alias));
+ assertEquals(Optional.of(200L), resolve(normalized));
+ }
+
+ @ParameterizedTest
+ @MethodSource("idLookupTypes")
+ void testPeerReplayEvictsAliasesAfterRenameAndNameReuse(MetadataObject.Type
type)
+ throws IOException {
+ MetadataObject alias = object(type, true);
+ MetadataObject normalized = object(type, false);
+ put(normalized, 100L);
+ assertAliasesShareBothCacheTiers(alias, normalized);
+ try (JcasbinChangeListener listener =
+ new JcasbinChangeListener(metadataCache, ownerCache, 3600L)) {
+ listener.onEntityChange(List.of(change(normalized, OperateType.ALTER)));
+ }
+ assertEquals(0L, metadataCache.size());
+ entities.remove(ident(normalized));
+ assertEquals(Optional.empty(), resolve(alias));
+ put(normalized, 300L);
+ assertEquals(Optional.of(300L), resolve(alias));
+ assertEquals(Optional.of(300L), resolve(normalized));
+ }
+
+ @ParameterizedTest
+ @MethodSource("idLookupTypes")
+ void testCaseSensitiveCatalogKeepsNamesDistinct(MetadataObject.Type type) {
+ when(catalog.capability()).thenReturn(Capability.DEFAULT);
+ MetadataObject upper = object(type, true);
+ MetadataObject lower = object(type, false);
+ assertSame(upper, MetadataIdConverter.normalizeMetadataObject(upper,
METALAKE));
+ put(upper, 100L);
+ put(lower, 200L);
+ assertEquals(Optional.of(100L), resolve(upper));
+ assertEquals(Optional.of(200L), resolve(lower));
+ assertEquals(2L, metadataCache.size());
+ AuthorizationUtils.notifyEntityNameIdMappingChange(ident(upper),
entityType(type));
+ put(upper, 300L);
+ assertEquals(Optional.of(300L), resolve(upper));
+ assertEquals(Optional.of(200L), resolve(lower));
+ }
+
+ @Test
+ void testTableDropHookEvictsTableAndColumnAliases() throws Exception {
+ MetadataObject table = object(MetadataObject.Type.TABLE, false);
+ MetadataObject column = object(MetadataObject.Type.COLUMN, false);
+ put(table, 100L);
+ // EntityStore does not independently load COLUMN IDs. Seed a descendant
key to verify the
+ // hook's prefix eviction, without inventing a ColumnEntity that
implements HasIdentifier.
+
metadataCache.put(JcasbinAuthorizationCacheKeys.metadataIdCacheKey(METALAKE,
column), 200L);
+ assertEquals(Optional.of(100L), resolve(object(MetadataObject.Type.TABLE,
true)));
+ TableDispatcher dispatcher = mock(TableDispatcher.class);
+ when(dispatcher.dropTable(ident(table))).thenReturn(true);
+ assertTrue(new TableHookDispatcher(dispatcher, () ->
null).dropTable(ident(table)));
+ assertEquals(0L, metadataCache.size());
+ put(table, 300L);
+ assertEquals(Optional.of(300L), resolve(object(MetadataObject.Type.TABLE,
true)));
+ assertFalse(
+ metadataCache
+
.getIfPresent(JcasbinAuthorizationCacheKeys.metadataIdCacheKey(METALAKE,
column))
+ .isPresent());
+ }
+
+ @Test
+ void testTableRenameHookEvictsOldAliasAndAllowsNameReuse() throws Exception {
+ MetadataObject old = object(MetadataObject.Type.TABLE, false);
+ MetadataObject alias = object(MetadataObject.Type.TABLE, true);
+ MetadataObject renamed = MetadataObjects.parse("cat.schema.renamed",
MetadataObject.Type.TABLE);
+ put(old, 100L);
+ assertEquals(Optional.of(100L), resolve(alias));
+ TableDispatcher dispatcher = mock(TableDispatcher.class);
+ TableChange rename = TableChange.rename("renamed");
+ when(dispatcher.alterTable(ident(old),
rename)).thenReturn(mock(Table.class));
+ new TableHookDispatcher(dispatcher, () -> null).alterTable(ident(old),
rename);
+ entities.remove(ident(old));
+ put(renamed, 100L);
+ assertEquals(Optional.empty(), resolve(alias));
+ assertEquals(Optional.of(100L), resolve(renamed));
+ put(old, 200L);
+ assertEquals(Optional.of(200L), resolve(alias));
+ }
+
+ @Test
+ void testSchemaDropHookAndPeerReplayEvictDescendantAliases() throws
Exception {
+ MetadataObject schema = object(MetadataObject.Type.SCHEMA, false);
+ MetadataObject table = object(MetadataObject.Type.TABLE, false);
+ MetadataObject column = object(MetadataObject.Type.COLUMN, false);
+ put(schema, 100L);
+ put(table, 200L);
+
metadataCache.put(JcasbinAuthorizationCacheKeys.metadataIdCacheKey(METALAKE,
column), 300L);
+ resolve(object(MetadataObject.Type.SCHEMA, true));
+ resolve(object(MetadataObject.Type.TABLE, true));
+ SchemaDispatcher dispatcher = mock(SchemaDispatcher.class);
+ when(dispatcher.dropSchema(ident(schema), true)).thenReturn(true);
+ assertTrue(new SchemaHookDispatcher(dispatcher).dropSchema(ident(schema),
true));
+ assertEquals(0L, metadataCache.size());
+ assertEquals(Optional.of(200L), resolve(object(MetadataObject.Type.TABLE,
true)));
+
metadataCache.put(JcasbinAuthorizationCacheKeys.metadataIdCacheKey(METALAKE,
column), 300L);
+ try (JcasbinChangeListener listener =
+ new JcasbinChangeListener(metadataCache, ownerCache, 3600L)) {
+ listener.onEntityChange(List.of(change(schema, OperateType.DROP)));
+ }
+ assertEquals(0L, metadataCache.size());
+ put(table, 400L);
+ assertEquals(Optional.of(400L), resolve(object(MetadataObject.Type.TABLE,
true)));
+ assertFalse(
+ metadataCache
+
.getIfPresent(JcasbinAuthorizationCacheKeys.metadataIdCacheKey(METALAKE,
column))
+ .isPresent());
+ }
+
+ @Test
+ void testColumnNormalizationHonorsEachScopeAndPreservesCatalogName() {
+ when(catalog.capability())
+ .thenReturn(
+ new Capability() {
+ @Override
+ public CapabilityResult caseSensitiveOnName(Scope scope) {
+ return scope == Scope.TABLE
+ ? CapabilityResult.SUPPORTED
+ : CapabilityResult.unsupported("case insensitive");
+ }
+ });
+ MetadataObject column =
+ MetadataObjects.parse("CAT.SCHEMA.Table.CoL",
MetadataObject.Type.COLUMN);
+ assertEquals(
+ MetadataObjects.parse("CAT.schema.Table.col",
MetadataObject.Type.COLUMN),
+ MetadataIdConverter.normalizeMetadataObject(column, "Metalake"));
+ }
+
+ @Test
+ void testLoaderUsesTheCanonicalNameOfItsCacheKey() {
+ MetadataObject alias = object(MetadataObject.Type.TABLE, true);
+ MetadataObject normalized = object(MetadataObject.Type.TABLE, false);
+ put(normalized, 100L);
+ put(alias, 200L);
+ // A catalog can be replaced between key normalization and the loader's
catalog lookup.
+ when(catalog.capability()).thenReturn(CASE_INSENSITIVE,
Capability.DEFAULT);
+ assertEquals(Optional.of(100L), resolve(alias));
+ assertEquals(Optional.of(100L), resolve(normalized));
+ assertEquals(1L, metadataCache.size());
+ }
+
+ @Test
+ void testNonCatalogScopesKeepNamesAndSkipCapabilityLookup() {
+ for (MetadataObject.Type type :
+ List.of(
+ MetadataObject.Type.METALAKE, MetadataObject.Type.CATALOG,
MetadataObject.Type.ROLE)) {
+ MetadataObject object = MetadataObjects.of(null, "MixedName", type);
+ assertSame(object, MetadataIdConverter.normalizeMetadataObject(object,
METALAKE));
+ }
+ verifyNoInteractions(catalogs);
+ }
+
+ @Test
+ void testMissingCatalogDoesNotReturnCachedId() {
+ MetadataObject table = object(MetadataObject.Type.TABLE, false);
+ put(table, 100L);
+ assertEquals(Optional.of(100L), resolve(table));
+ doThrow(new NoSuchCatalogException("Missing catalog"))
+ .when(catalogs)
+ .doWithCatalog(any(), any());
+ assertEquals(Optional.empty(), resolve(table));
+ }
+
+ @Test
+ void testSemanticModelLeafRemainsCaseSensitiveInInsensitiveCatalog() {
+ MetadataObject upper =
+ MetadataObjects.parse("cat.SCHEMA.Model",
MetadataObject.Type.SEMANTIC_MODEL);
+ MetadataObject lower =
+ MetadataObjects.parse("cat.schema.model",
MetadataObject.Type.SEMANTIC_MODEL);
+ put(object(MetadataObject.Type.SEMANTIC_MODEL, false), 100L);
+ put(lower, 200L);
+ assertEquals(Optional.of(100L), resolve(upper));
+ assertEquals(Optional.of(200L), resolve(lower));
+ assertEquals(2L, metadataCache.size());
+ }
+
+ private void assertAliasesShareBothCacheTiers(MetadataObject alias,
MetadataObject normalized)
+ throws IOException {
+ AuthorizationRequestContext context = new AuthorizationRequestContext();
+ assertEquals(Optional.of(100L), lookups.resolveMetadataId(alias, METALAKE,
context));
+ assertEquals(Optional.of(100L), lookups.resolveMetadataId(normalized,
METALAKE, context));
+ assertEquals(Optional.of(100L), resolve(alias));
+ assertEquals(1L, metadataCache.size());
+ assertTrue(
+ metadataCache
+
.getIfPresent(JcasbinAuthorizationCacheKeys.metadataIdCacheKey(METALAKE,
normalized))
+ .isPresent());
+ assertFalse(
+ metadataCache
+
.getIfPresent(JcasbinAuthorizationCacheKeys.metadataIdCacheKey(METALAKE, alias))
+ .isPresent());
+ verify(store, times(1))
+ .get(
+ ident(normalized),
+ entityType(normalized.type()),
+ EntityClassMapper.getEntityClass(entityType(normalized.type())));
+ }
+
+ private Optional<Long> resolve(MetadataObject object) {
+ return lookups.resolveMetadataId(object, METALAKE, new
AuthorizationRequestContext());
+ }
+
+ private void put(MetadataObject object, long id) {
+ Entity entity =
mock(EntityClassMapper.getEntityClass(entityType(object.type())));
+ when(((HasIdentifier) entity).id()).thenReturn(id);
+ entities.put(ident(object), entity);
+ }
+
+ private static NameIdentifier ident(MetadataObject object) {
+ return MetadataObjectUtil.toEntityIdent(METALAKE, object);
+ }
+
+ private static Entity.EntityType entityType(MetadataObject.Type type) {
+ return MetadataObjectUtil.toEntityType(type);
+ }
+
+ private static EntityChangeRecord change(MetadataObject object, OperateType
operation) {
+ return new EntityChangeRecord(
+ 1L,
+ METALAKE,
+ object.type().name(),
+ EntityChangeLogNameIdentifierCodec.encode(ident(object)),
+ operation,
+ 1L);
+ }
+
+ private static MetadataObject object(MetadataObject.Type type, boolean
alias) {
+ String name;
+ switch (type) {
+ case SCHEMA:
+ name = alias ? "cat.SCHEMA" : "cat.schema";
+ break;
+ case COLUMN:
+ name = alias ? "cat.SCHEMA.OBJECT.CoL" : "cat.schema.object.col";
+ break;
+ case SEMANTIC_MODEL:
+ name = alias ? "cat.SCHEMA.Model" : "cat.schema.Model";
+ break;
+ case TABLE:
+ case MODEL:
+ case FILESET:
+ case TOPIC:
+ name = alias ? "cat.SCHEMA.OBJECT" : "cat.schema.object";
+ break;
+ default:
+ throw new IllegalArgumentException("Add normalization coverage for " +
type);
+ }
+ return MetadataObjects.parse(name, type);
+ }
+
+ private static Stream<MetadataObject.Type> idLookupTypes() throws
IllegalAccessException {
+ // Only HasIdentifier entities satisfy EntityStore.get's production
contract. COLUMN remains
+ // covered by normalization and descendant-key eviction, not by a
fabricated ID-load fixture.
+ return catalogScopedTypes()
+ .filter(
+ type ->
+ HasIdentifier.class.isAssignableFrom(
+ EntityClassMapper.getEntityClass(entityType(type))));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Stream<MetadataObject.Type> catalogScopedTypes() throws
IllegalAccessException {
+ // Derive coverage from the production registry: a newly mapped type
cannot silently be missed.
+ Map<MetadataObject.Type, Capability.Scope> scopes =
+ (Map<MetadataObject.Type, Capability.Scope>)
+ FieldUtils.readDeclaredStaticField(
+ MetadataIdConverter.class, "METADATA_SCOPE_MAPPING", true);
+ return scopes.keySet().stream();
Review Comment:
Addressed in 764d6b75d. Added a documented @VisibleForTesting accessor
returning the immutable set of catalog-scoped types from the production
mapping. The parameterized test now calls that accessor rather than reflecting
on METADATA_SCOPE_MAPPING by field name, while still automatically covering
newly mapped types.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]