Skip to content

Commit 65b6b2a

Browse files
authored
concord-db, concord-server: cache external app user mapping in database (#1244)
1 parent dbedfd6 commit 65b6b2a

File tree

9 files changed

+165
-5
lines changed

9 files changed

+165
-5
lines changed

server/db/src/main/resources/com/walmartlabs/concord/server/db/liquibase.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,5 @@
119119
<include file="v2.22.0.xml" relativeToChangelogFile="true"/>
120120
<include file="v2.23.0.xml" relativeToChangelogFile="true"/>
121121
<include file="v2.31.0.xml" relativeToChangelogFile="true"/>
122+
<include file="v2.35.0.xml" relativeToChangelogFile="true"/>
122123
</databaseChangeLog>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<databaseChangeLog
3+
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd">
6+
7+
<changeSet id="2510000" author="[email protected]">
8+
<createTable tableName="EXTERNAL_APP_USERS">
9+
<column name="EXTERNAL_USER_ID" type="varchar(512)">
10+
<constraints primaryKey="true" nullable="false"/>
11+
</column>
12+
<column name="USER_ID" type="uuid">
13+
<constraints nullable="false"/>
14+
</column>
15+
</createTable>
16+
17+
<addForeignKeyConstraint baseTableName="EXTERNAL_APP_USERS"
18+
baseColumnNames="USER_ID"
19+
constraintName="FK_EXTERNAL_APP_USERS_USER_ID"
20+
referencedTableName="USERS"
21+
referencedColumnNames="USER_ID"
22+
onDelete="CASCADE"/>
23+
</changeSet>
24+
</databaseChangeLog>

server/dist/src/main/resources/concord-server.conf

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,10 @@ concord-server {
516516
useSenderLdapDn = true
517517

518518
# if enabled, use the payload's 'sender.email' to find the initiator
519-
userSenderEmail = false
519+
useSenderEmail = false
520+
521+
# if enabled, stores a mapping of github user id -> concord user id in the DB
522+
enableExternalUserIdMappingCache = false
520523

521524
# Number of webhook sender email lookups to cache
522525
senderEmailCacheSize = 1024

server/impl/src/main/java/com/walmartlabs/concord/server/cfg/GithubConfiguration.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ public class GithubConfiguration implements GitHubAppInstallationConfig {
4141
private boolean useSenderLdapDn;
4242

4343
@Inject
44-
@Config("github.userSenderEmail")
45-
private boolean userSenderEmail;
44+
@Config("github.useSenderEmail")
45+
private boolean useSenderEmail;
4646

4747
@Inject
4848
@Config("github.senderEmailCacheSize")
@@ -52,6 +52,10 @@ public class GithubConfiguration implements GitHubAppInstallationConfig {
5252
@Config("github.senderEmailCacheDuration")
5353
private Duration senderEmailCacheDuration;
5454

55+
@Inject
56+
@Config("github.enableExternalUserIdMappingCache")
57+
private boolean enableExternalUserIdMappingCache;
58+
5559
@Inject
5660
@Config("github.logEvents")
5761
private boolean logEvents;
@@ -81,7 +85,11 @@ public boolean isUseSenderLdapDn() {
8185
}
8286

8387
public boolean isUseSenderEmail() {
84-
return userSenderEmail;
88+
return useSenderEmail;
89+
}
90+
91+
public boolean isEnableExternalUserIdMappingCache() {
92+
return enableExternalUserIdMappingCache;
8593
}
8694

8795
public long senderEmailCacheSize() {

server/impl/src/main/java/com/walmartlabs/concord/server/events/GithubEventResource.java

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@
8484
import static com.walmartlabs.concord.common.MemoSupplier.memo;
8585
import static com.walmartlabs.concord.server.events.github.Constants.COMMIT_ID_KEY;
8686
import static com.walmartlabs.concord.server.events.github.Constants.EVENT_SOURCE;
87+
import static com.walmartlabs.concord.server.events.github.Constants.NODE_ID_KEY;
88+
import static com.walmartlabs.concord.server.events.github.Constants.SENDER_KEY;
89+
import static com.walmartlabs.concord.server.events.github.Constants.URL_KEY;
8790

8891
/**
8992
* Handles external GitHub events.
@@ -295,8 +298,15 @@ public GithubEventInitiatorSupplier(UserManager userManager, LdapManager ldapMan
295298

296299
@Override
297300
public UserEntry get() {
301+
// GitHub user -> Concord user mapping may already exist in DB
302+
UserEntry fromDbMapping = findUserInDbMapping();
303+
304+
if (fromDbMapping != null) {
305+
return fromDbMapping;
306+
}
307+
308+
// don't try to match against payload sender's ldap_dn or email if they aren't present
298309
if (!githubCfg.isUseSenderLdapDn() && !githubCfg.isUseSenderEmail()) {
299-
// don't try to match against payload sender's ldap_dn or email
300310
return fallback.get();
301311
}
302312

@@ -305,6 +315,7 @@ public UserEntry get() {
305315
if (githubCfg.isUseSenderLdapDn()) {
306316
UserEntry fromDn = findSenderDnInLdap();
307317
if (fromDn != null) {
318+
addUserDbMapping(fromDn);
308319
return fromDn;
309320
}
310321
}
@@ -313,6 +324,7 @@ public UserEntry get() {
313324
if (githubCfg.isUseSenderEmail()) {
314325
UserEntry fromEmail = findSenderEmailInLdap();
315326
if (fromEmail != null) {
327+
addUserDbMapping(fromEmail);
316328
return fromEmail;
317329
}
318330
}
@@ -321,6 +333,49 @@ public UserEntry get() {
321333
return fallback.get();
322334
}
323335

336+
private UserEntry findUserInDbMapping() {
337+
if (!githubCfg.isEnableExternalUserIdMappingCache()) {
338+
return null;
339+
}
340+
341+
String senderUrl = payload.getUrl(SENDER_KEY);
342+
String senderNodeId = payload.getNodeId(SENDER_KEY);
343+
String externalId = formatExternalId(senderUrl, senderNodeId);
344+
345+
if (externalId == null) {
346+
return null;
347+
}
348+
return userManager.getUserFromExternalMapping(externalId).orElse(null);
349+
}
350+
351+
private void addUserDbMapping(UserEntry user) {
352+
if (!githubCfg.isEnableExternalUserIdMappingCache()) {
353+
return;
354+
}
355+
356+
String senderUrl = payload.getUrl(SENDER_KEY);
357+
String senderNodeId = payload.getNodeId(SENDER_KEY);
358+
String externalId = formatExternalId(senderUrl, senderNodeId);
359+
360+
if (externalId == null) {
361+
return;
362+
}
363+
364+
userManager.createExternalUserMapping(user.getId(), externalId);
365+
}
366+
367+
private static String formatExternalId(String url, String userId) {
368+
if (url == null || userId == null) {
369+
return null;
370+
}
371+
372+
if (url.isBlank() || userId.isBlank()) {
373+
return null;
374+
}
375+
376+
return String.format("github_%s=%s,%s=%s", URL_KEY, url, NODE_ID_KEY, userId);
377+
}
378+
324379
private UserEntry findSenderDnInLdap() {
325380
String ldapDn = payload.getSenderLdapDn();
326381
if (ldapDn == null || ldapDn.isBlank()) {

server/impl/src/main/java/com/walmartlabs/concord/server/events/github/Constants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ public final class Constants {
4343
public static final String QUERY_PARAMS_KEY = "queryParams";
4444
public static final String BASE_KEY = "base";
4545
public static final String HEAD_KEY = "head";
46+
public static final String NODE_ID_KEY = "node_id";
4647
public static final String REF_KEY = "ref";
48+
public static final String URL_KEY = "url";
4749

4850
public static final String GITHUB_ORG_KEY = "githubOrg";
4951
public static final String GITHUB_REPO_KEY = "githubRepo";

server/impl/src/main/java/com/walmartlabs/concord/server/events/github/Payload.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,19 @@ public boolean hasPullRequestEntry() {
210210
return raw().containsKey(PULL_REQUEST_EVENT);
211211
}
212212

213+
public String getUrl(String attribute) {
214+
Map<String, Object> m = MapUtils.getMap(raw(), attribute, Map.of());
215+
return MapUtils.getString(m, URL_KEY);
216+
}
217+
218+
/**
219+
* @return graphql node id for the given attribute
220+
*/
221+
public String getNodeId(String attribute) {
222+
Map<String, Object> m = MapUtils.getMap(raw(), attribute, Map.of());
223+
return MapUtils.getString(m, NODE_ID_KEY);
224+
}
225+
213226
public String getPullRequestBaseUrl() {
214227
Map<String, Object> pullRequest = getPullRequestAttribute(this.raw());
215228
return getPullRequestCloneUrl(pullRequest, BASE_KEY);

server/impl/src/main/java/com/walmartlabs/concord/server/user/UserDao.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,44 @@ public UserEntry get(UUID id) {
160160
return getUserInfo(tx, r);
161161
}
162162

163+
public UserEntry getUserFromExternalMapping(String externalId) {
164+
DSLContext tx = dsl();
165+
166+
Record9<UUID, String, String, String, String, String, Boolean, Boolean, OffsetDateTime> r =
167+
tx.select(USERS.USER_ID, USERS.USER_TYPE, USERS.USERNAME, USERS.DOMAIN, USERS.DISPLAY_NAME, USERS.USER_EMAIL, USERS.IS_DISABLED, USERS.IS_PERMANENTLY_DISABLED, USERS.DISABLED_DATE)
168+
.from(USERS, EXTERNAL_APP_USERS)
169+
.where(USERS.USER_ID.eq(EXTERNAL_APP_USERS.USER_ID))
170+
.and(EXTERNAL_APP_USERS.EXTERNAL_USER_ID.eq(externalId))
171+
.fetchOne();
172+
173+
if (r == null) {
174+
return null;
175+
}
176+
177+
return getUserInfo(tx, r);
178+
}
179+
180+
/**
181+
* Creates a mapping from an external user to an existing Concord user. The
182+
* external identifier may not be a singular, exact, ID for the system. The
183+
* context(s) creating and retrieving the mapping must understand the construction
184+
* of the identifier. Typically, it should be a formatting along the lines
185+
* of <code>{external_system} + {actual_user_id}</code> to avoid clashing of
186+
* IDs that are unique to the system but potentially duplicated across other
187+
* external systems that have been integrated (e.g. multiple GitHub instances).
188+
* @param userId existing Concord user ID
189+
* @param externalId identifier for user in an external system
190+
*/
191+
public void createExternalUserMapping(UUID userId, String externalId) {
192+
tx(tx -> tx.insertInto(EXTERNAL_APP_USERS)
193+
.columns(EXTERNAL_APP_USERS.EXTERNAL_USER_ID, EXTERNAL_APP_USERS.USER_ID)
194+
.values(externalId, userId)
195+
.onConflict(EXTERNAL_APP_USERS.EXTERNAL_USER_ID)
196+
.doUpdate()
197+
.set(EXTERNAL_APP_USERS.USER_ID, userId)
198+
.execute());
199+
}
200+
163201
/**
164202
* Returns the ID of the specified user.
165203
* Note that {@code username} and {@code domain} are case-insensitive and forced to lower case.

server/impl/src/main/java/com/walmartlabs/concord/server/user/UserManager.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,22 @@ public Optional<UUID> getId(String username, String userDomain, UserType type) {
108108
return Optional.ofNullable(userDao.getId(username, userDomain, type));
109109
}
110110

111+
public Optional<UserEntry> getUserFromExternalMapping(String externalId) {
112+
if (externalId == null) {
113+
return Optional.empty();
114+
}
115+
116+
return Optional.ofNullable(userDao.getUserFromExternalMapping(externalId));
117+
}
118+
119+
public void createExternalUserMapping(UUID concordUserId, String externalId) {
120+
if (externalId == null) {
121+
return;
122+
}
123+
124+
userDao.createExternalUserMapping(concordUserId, externalId);
125+
}
126+
111127
public Optional<UserEntry> update(UUID userId, String displayName, String email, UserType userType, boolean isDisabled, Set<String> roles) {
112128
UserEntry prevEntry = userDao.get(userId);
113129
if (prevEntry == null) {

0 commit comments

Comments
 (0)