Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit da1c856

Browse files
authored
Merge pull request #51 from billkalter/fix-internal-id-reindexing
Fixed errors grandfathering-in pre-existing API keys to internal IDs
2 parents 7d9d760 + 9bbd8bb commit da1c856

3 files changed

Lines changed: 135 additions & 20 deletions

File tree

auth/auth-core/src/main/java/com/bazaarvoice/emodb/auth/apikey/ApiKey.java

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.fasterxml.jackson.annotation.JsonCreator;
55
import com.fasterxml.jackson.annotation.JsonProperty;
66
import com.google.common.collect.ImmutableSet;
7-
import com.google.common.hash.Hashing;
87

98
import java.util.List;
109
import java.util.Set;
@@ -23,20 +22,6 @@ public ApiKey(@JsonProperty("id") String key,
2322
@JsonProperty("internalId") String internalId,
2423
@JsonProperty("roles") List<String> roles) {
2524

26-
// API keys have been in use since before internal IDs were introduced. To grandfather in those keys we'll
27-
// use a hash of the API key as the internal ID.
28-
this(key, resolveInternalId(key, internalId), ImmutableSet.copyOf(roles));
29-
}
30-
31-
private static String resolveInternalId(String key, String internalId) {
32-
if (internalId != null) {
33-
return internalId;
34-
}
35-
// SHA-256 is a little heavy but it has two advantages:
36-
// 1. It is the same algorithm currently used to store API keys by the permission manager so there isn't a
37-
// potential conflict between keys.
38-
// 2. The API keys are cached by Shiro so this conversion will only take place when the key needs to
39-
// be (re)loaded.
40-
return Hashing.sha256().hashUnencodedChars(key).toString();
25+
this(key, internalId, ImmutableSet.copyOf(roles));
4126
}
4227
}

auth/auth-store/src/main/java/com/bazaarvoice/emodb/auth/identity/TableAuthIdentityManager.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.google.common.base.Strings;
1818
import com.google.common.collect.ImmutableList;
1919
import com.google.common.collect.ImmutableMap;
20+
import com.google.common.collect.Maps;
2021
import com.google.common.hash.HashFunction;
2122

2223
import javax.annotation.Nullable;
@@ -37,6 +38,7 @@
3738
public class TableAuthIdentityManager<T extends AuthIdentity> implements AuthIdentityManager<T> {
3839

3940
private final static String ID = "id";
41+
private final static String INTERNAL_ID = "internalId";
4042
private final static String MASKED_ID = "maskedId";
4143
private final static String HASHED_ID = "hashedId";
4244

@@ -80,12 +82,23 @@ private T convertDataStoreEntryToIdentity(String id, Map<String, Object> map) {
8082
return null;
8183
}
8284

85+
// Make a copy of the map to avoid mutating the method parameter as a side-effect
86+
Map<String, Object> identityMap = Maps.newHashMap(map);
87+
88+
// Identities have been in use since before internal IDs were introduced. To grandfather in those keys we'll
89+
// use the hash of the identity's ID as the internal ID.
90+
if (!identityMap.containsKey(INTERNAL_ID)) {
91+
identityMap.put(INTERNAL_ID, Intrinsic.getId(map));
92+
}
93+
94+
// Remove all intrinsics
95+
identityMap.keySet().removeAll(Intrinsic.DATA_FIELDS);
96+
8397
// The entry is stored without the original ID, so add it back
84-
map.keySet().removeAll(Intrinsic.DATA_FIELDS);
85-
map.remove(MASKED_ID);
86-
map.put(ID, id);
98+
identityMap.remove(MASKED_ID);
99+
identityMap.put(ID, id);
87100

88-
return JsonHelper.convert(map, _authIdentityClass);
101+
return JsonHelper.convert(identityMap, _authIdentityClass);
89102
}
90103

91104
@Override
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package test.integration.auth;
2+
3+
import com.bazaarvoice.emodb.auth.apikey.ApiKey;
4+
import com.bazaarvoice.emodb.auth.identity.TableAuthIdentityManager;
5+
import com.bazaarvoice.emodb.sor.api.AuditBuilder;
6+
import com.bazaarvoice.emodb.sor.api.DataStore;
7+
import com.bazaarvoice.emodb.sor.api.Intrinsic;
8+
import com.bazaarvoice.emodb.sor.core.test.InMemoryDataStore;
9+
import com.bazaarvoice.emodb.sor.delta.Deltas;
10+
import com.bazaarvoice.emodb.sor.uuid.TimeUUIDs;
11+
import com.codahale.metrics.MetricRegistry;
12+
import com.google.common.collect.ImmutableList;
13+
import com.google.common.collect.ImmutableMap;
14+
import com.google.common.collect.ImmutableSet;
15+
import com.google.common.hash.Hashing;
16+
import org.testng.annotations.Test;
17+
18+
import java.util.Map;
19+
import java.util.Set;
20+
21+
import static org.testng.Assert.assertEquals;
22+
import static org.testng.Assert.assertFalse;
23+
import static org.testng.Assert.assertNotNull;
24+
25+
public class TableAuthIdentityManagerTest {
26+
27+
/**
28+
* There are two tables which store identities in TableAuthIdentityManager: One table keyed by a hash of the
29+
* API key, and an index table ID'd by the internal ID which contains the API key hash. This second table is used
30+
* to look up API keys by internal ID. It should be rare, but it is possible for an API key record to exist
31+
* without a corresponding internal ID. One possible way for this to happen is grandfathered in API keys
32+
* created before the introduction of internal IDs. TableAuthIdentityManager should rebuild the index
33+
* when there is a missing or incorrect index record. This test verifies that works as expected.
34+
*/
35+
@Test
36+
public void testRebuildInternalIdIndex() {
37+
DataStore dataStore = new InMemoryDataStore(new MetricRegistry());
38+
TableAuthIdentityManager<ApiKey> tableAuthIdentityManager = new TableAuthIdentityManager<>(
39+
ApiKey.class, dataStore, "__auth:keys", "__auth:internal_ids", "app_global:sys", Hashing.sha256());
40+
41+
ApiKey apiKey = new ApiKey("testkey", "id0", ImmutableSet.of("role1", "role2"));
42+
apiKey.setOwner("testowner");
43+
tableAuthIdentityManager.updateIdentity(apiKey);
44+
45+
// Verify both tables have been written
46+
47+
String keyTableId = Hashing.sha256().hashUnencodedChars("testkey").toString();
48+
49+
Map<String, Object> keyMap = dataStore.get("__auth:keys", keyTableId);
50+
assertFalse(Intrinsic.isDeleted(keyMap));
51+
assertEquals(keyMap.get("owner"), "testowner");
52+
53+
Map<String, Object> indexMap = dataStore.get("__auth:internal_ids", "id0");
54+
assertFalse(Intrinsic.isDeleted(indexMap));
55+
assertEquals(indexMap.get("hashedId"), keyTableId);
56+
57+
// Deliberately delete the index map record
58+
dataStore.update("__auth:internal_ids", "id0", TimeUUIDs.newUUID(), Deltas.delete(),
59+
new AuditBuilder().setComment("test delete").build());
60+
61+
// Verify that a lookup by internal ID works
62+
Set<String> roles = tableAuthIdentityManager.getRolesByInternalId("id0");
63+
assertEquals(roles, ImmutableSet.of("role1", "role2"));
64+
65+
// Verify that the index record is re-created
66+
indexMap = dataStore.get("__auth:internal_ids", "id0");
67+
assertFalse(Intrinsic.isDeleted(indexMap));
68+
assertEquals(indexMap.get("hashedId"), keyTableId);
69+
}
70+
71+
@Test
72+
public void testGrandfatheredInInternalId() {
73+
DataStore dataStore = new InMemoryDataStore(new MetricRegistry());
74+
TableAuthIdentityManager<ApiKey> tableAuthIdentityManager = new TableAuthIdentityManager<>(
75+
ApiKey.class, dataStore, "__auth:keys", "__auth:internal_ids", "app_global:sys", Hashing.sha256());
76+
77+
// Perform an operation on tableAuthIdentityManager to force it to create API key tables; the actual
78+
// operation doesn't matter.
79+
tableAuthIdentityManager.getIdentity("ignore");
80+
81+
String id = "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllll";
82+
String hash = Hashing.sha256().hashUnencodedChars(id).toString();
83+
84+
// Write out a record which mimics the pre-internal-id format. Notably missing is the "internalId" attribute.
85+
Map<String, Object> oldIdentityMap = ImmutableMap.<String, Object>builder()
86+
.put("maskedId", "aaaa****************************************llll")
87+
.put("owner", "someone")
88+
.put("description", "something")
89+
.put("roles", ImmutableList.of("role1", "role2"))
90+
.build();
91+
92+
dataStore.update("__auth:keys", hash, TimeUUIDs.newUUID(), Deltas.literal(oldIdentityMap),
93+
new AuditBuilder().setComment("test grandfathering").build());
94+
95+
// Verify the record can be read by ID. The key's internal ID will be the hashed ID.
96+
ApiKey apiKey = tableAuthIdentityManager.getIdentity(id);
97+
assertNotNull(apiKey);
98+
assertEquals(apiKey.getId(), id);
99+
assertEquals(apiKey.getInternalId(), hash);
100+
assertEquals(apiKey.getOwner(), "someone");
101+
assertEquals(apiKey.getDescription(), "something");
102+
assertEquals(apiKey.getRoles(), ImmutableList.of("role1", "role2"));
103+
104+
// Verify that a lookup by internal ID works
105+
Set<String> roles = tableAuthIdentityManager.getRolesByInternalId(hash);
106+
assertEquals(roles, ImmutableSet.of("role1", "role2"));
107+
108+
// Verify that the index record was created with the hashed ID as the internal ID
109+
Map<String, Object> indexMap = dataStore.get("__auth:internal_ids", hash);
110+
assertFalse(Intrinsic.isDeleted(indexMap));
111+
assertEquals(indexMap.get("hashedId"), hash);
112+
113+
// Verify lookup by internal ID still works with the index record in place
114+
roles = tableAuthIdentityManager.getRolesByInternalId(hash);
115+
assertEquals(roles, ImmutableSet.of("role1", "role2"));
116+
}
117+
}

0 commit comments

Comments
 (0)