Skip to content

Commit 62c102d

Browse files
committed
Add permanent cache option and cleanup thread for multi workspace authorization
1 parent d09b4f5 commit 62c102d

File tree

5 files changed

+146
-9
lines changed

5 files changed

+146
-9
lines changed

bolt/src/main/java/com/slack/api/bolt/AppConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,17 @@ public boolean isDistributedApp() {
7070
private boolean oAuthCallbackEnabled = false;
7171

7272

73+
/**
74+
* Returns true if auth.test call result cache in SingleTeamAuthorization or MultiTeamsAuthorization middleware
75+
* is enabled. The default is false.
76+
*/
7377
@Builder.Default
7478
private boolean authTestCacheEnabled = false;
79+
80+
/**
81+
* Returns the millisecond value to keep cached auth.test response in cache.
82+
* Negative value indicates the cache is permanent. The default is 3000 milliseconds.
83+
*/
7584
@Builder.Default
7685
private long authTestCacheExpirationMillis = 3000L;
7786

bolt/src/main/java/com/slack/api/bolt/middleware/builtin/MultiTeamsAuthorization.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@
1515
import com.slack.api.methods.SlackApiException;
1616
import com.slack.api.methods.response.auth.AuthTestResponse;
1717
import com.slack.api.model.block.LayoutBlock;
18+
import com.slack.api.util.thread.ExecutorServiceFactory;
1819
import lombok.AllArgsConstructor;
1920
import lombok.Data;
2021
import lombok.extern.slf4j.Slf4j;
2122

2223
import java.io.IOException;
2324
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Optional;
2427
import java.util.concurrent.ConcurrentHashMap;
2528
import java.util.concurrent.ConcurrentMap;
29+
import java.util.concurrent.ScheduledExecutorService;
30+
import java.util.concurrent.TimeUnit;
2631

2732
import static com.slack.api.bolt.middleware.MiddlewareOps.isNoAuthRequiredRequest;
2833
import static com.slack.api.bolt.response.ResponseTypes.ephemeral;
@@ -45,6 +50,7 @@ static class CachedAuthTestResponse {
4550

4651
// token -> auth.test response
4752
private final ConcurrentMap<String, CachedAuthTestResponse> tokenToAuthTestCache = new ConcurrentHashMap<>();
53+
private final Optional<ScheduledExecutorService> tokenToAuthTestCacheCleaner;
4854

4955
private boolean alwaysRequestUserTokenNeeded;
5056

@@ -60,6 +66,40 @@ public MultiTeamsAuthorization(AppConfig config, InstallationService installatio
6066
this.config = config;
6167
this.installationService = installationService;
6268
setAlwaysRequestUserTokenNeeded(config.isAlwaysRequestUserTokenNeeded());
69+
if (config.isAuthTestCacheEnabled()) {
70+
boolean permanentCacheEnabled = config.getAuthTestCacheExpirationMillis() < 0;
71+
if (permanentCacheEnabled) {
72+
this.tokenToAuthTestCacheCleaner = Optional.empty();
73+
} else {
74+
this.tokenToAuthTestCacheCleaner = Optional.of(buildTokenToAuthTestCacheCleaner(() -> {
75+
long expirationMillis = System.currentTimeMillis() - config.getAuthTestCacheExpirationMillis();
76+
for (Map.Entry<String, CachedAuthTestResponse> each : tokenToAuthTestCache.entrySet()) {
77+
if (each.getValue() == null || each.getValue().getCachedMillis() < expirationMillis) {
78+
tokenToAuthTestCache.remove(each.getKey());
79+
}
80+
}
81+
}));
82+
}
83+
} else {
84+
this.tokenToAuthTestCacheCleaner = Optional.empty();
85+
}
86+
}
87+
88+
private ScheduledExecutorService buildTokenToAuthTestCacheCleaner(Runnable task) {
89+
String threadGroupName = MultiTeamsAuthorization.class.getSimpleName();
90+
ScheduledExecutorService tokenToAuthTestCacheCleaner =
91+
ExecutorServiceFactory.createDaemonThreadScheduledExecutor(threadGroupName);
92+
tokenToAuthTestCacheCleaner.scheduleAtFixedRate(task, 120_000, 30_000, TimeUnit.MILLISECONDS);
93+
log.debug("The tokenToAuthTestCacheCleaner (daemon thread) started");
94+
return tokenToAuthTestCacheCleaner;
95+
}
96+
97+
@Override
98+
protected void finalize() throws Throwable {
99+
if (this.tokenToAuthTestCacheCleaner.isPresent()) {
100+
this.tokenToAuthTestCacheCleaner.get().shutdown();
101+
}
102+
super.finalize();
63103
}
64104

65105
@Override
@@ -150,6 +190,10 @@ protected AuthTestResponse callAuthTest(String token, AppConfig config, MethodsC
150190
if (config.isAuthTestCacheEnabled()) {
151191
CachedAuthTestResponse cachedResponse = tokenToAuthTestCache.get(token);
152192
if (cachedResponse != null) {
193+
boolean permanentCacheEnabled = config.getAuthTestCacheExpirationMillis() < 0;
194+
if (permanentCacheEnabled) {
195+
return cachedResponse.getResponse();
196+
}
153197
long millisToExpire = cachedResponse.getCachedMillis() + config.getAuthTestCacheExpirationMillis();
154198
if (millisToExpire > System.currentTimeMillis()) {
155199
return cachedResponse.getResponse();

bolt/src/main/java/com/slack/api/bolt/middleware/builtin/SingleTeamAuthorization.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,16 @@ public Response apply(Request req, Response resp, MiddlewareChain chain) throws
8484

8585
protected AuthTestResponse callAuthTest(AppConfig config, MethodsClient client) throws IOException, SlackApiException {
8686
if (config.isAuthTestCacheEnabled()) {
87-
long millisToExpire = lastCachedMillis.get() + config.getAuthTestCacheExpirationMillis();
88-
if (cachedAuthTestResponse.isPresent() && millisToExpire > System.currentTimeMillis()) {
89-
return cachedAuthTestResponse.get();
87+
if (cachedAuthTestResponse.isPresent()) {
88+
boolean permanentCacheEnabled = config.getAuthTestCacheExpirationMillis() < 0;
89+
if (permanentCacheEnabled) {
90+
return cachedAuthTestResponse.get();
91+
}
92+
long millisToExpire = lastCachedMillis.get() + config.getAuthTestCacheExpirationMillis();
93+
long currentMillis = System.currentTimeMillis();
94+
if (millisToExpire > currentMillis) {
95+
return cachedAuthTestResponse.get();
96+
}
9097
}
9198
AuthTestResponse response = client.authTest(r -> r.token(config.getSingleTeamBotToken()));
9299
cachedAuthTestResponse = Optional.of(response); // response here is not null for sure

bolt/src/test/java/test_locally/app/MultiTeamsAuthTestCacheTest.java

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public static void tearDown() throws Exception {
7171

7272
@Test
7373
public void cacheEnabled() throws Exception {
74-
App app = buildApp(true);
74+
App app = buildApp(true, null);
7575
app.globalShortcut("test-global-shortcut", (req, ctx) -> ctx.ack());
7676

7777
String requestBody = "payload=" + URLEncoder.encode(realPayload, "UTF-8");
@@ -103,9 +103,43 @@ public void cacheEnabled() throws Exception {
103103
assertEquals(503L, response.getStatusCode().longValue());
104104
}
105105

106+
@Test
107+
public void permanentCacheEnabled() throws Exception {
108+
App app = buildApp(true, -1L);
109+
app.globalShortcut("test-global-shortcut", (req, ctx) -> ctx.ack());
110+
111+
String requestBody = "payload=" + URLEncoder.encode(realPayload, "UTF-8");
112+
113+
Map<String, List<String>> rawHeaders = new HashMap<>();
114+
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
115+
setRequestHeaders(requestBody, rawHeaders, timestamp);
116+
117+
GlobalShortcutRequest req = new GlobalShortcutRequest(requestBody, realPayload, new RequestHeaders(rawHeaders));
118+
assertEquals("seratch", req.getPayload().getUser().getUsername()); // a bit different from message_actions payload
119+
120+
Response response = app.run(req);
121+
assertEquals(200L, response.getStatusCode().longValue());
122+
123+
response = app.run(req);
124+
assertEquals(200L, response.getStatusCode().longValue());
125+
126+
Thread.sleep(300L);
127+
response = app.run(req);
128+
assertEquals(200L, response.getStatusCode().longValue());
129+
130+
Thread.sleep(300L);
131+
response = app.run(req);
132+
assertEquals(200L, response.getStatusCode().longValue());
133+
134+
Thread.sleep(3000L);
135+
136+
response = app.run(req);
137+
assertEquals(200L, response.getStatusCode().longValue());
138+
}
139+
106140
@Test
107141
public void cacheDisabled() throws Exception {
108-
App app = buildApp(false);
142+
App app = buildApp(false, null);
109143
app.globalShortcut("test-global-shortcut", (req, ctx) -> ctx.ack());
110144

111145
String requestBody = "payload=" + URLEncoder.encode(realPayload, "UTF-8");
@@ -124,14 +158,18 @@ public void cacheDisabled() throws Exception {
124158
assertEquals(503L, response.getStatusCode().longValue());
125159
}
126160

127-
App buildApp(boolean authTestCacheEnabled) {
128-
App app = new App(AppConfig.builder()
161+
App buildApp(boolean authTestCacheEnabled, Long ttlMillis) {
162+
AppConfig config = AppConfig.builder()
129163
.authTestCacheEnabled(authTestCacheEnabled)
130164
.signingSecret(secret)
131165
.clientId("test")
132166
.clientSecret("test-test")
133167
.slack(slack)
134-
.build());
168+
.build();
169+
if (ttlMillis != null) {
170+
config.setAuthTestCacheExpirationMillis(ttlMillis);
171+
}
172+
App app = new App(config);
135173
app.service(new InstallationService() {
136174
@Override
137175
public boolean isHistoricalDataEnabled() {
@@ -161,7 +199,7 @@ public Bot findBot(String enterpriseId, String teamId) {
161199
bot.setBotAccessToken("B111");
162200
bot.setBotUserId("U111");
163201
bot.setInstalledAt(System.currentTimeMillis());
164-
bot.setBotAccessToken("xoxb-1234567890-123456789012-12345678901234567890" + authTestCacheEnabled);
202+
bot.setBotAccessToken("xoxb-1234567890-123456789012-12345678901234567890" + authTestCacheEnabled + ttlMillis);
165203
return bot;
166204
}
167205

bolt/src/test/java/test_locally/app/SingleTeamAuthTestCacheTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,45 @@ public void cacheEnabled() throws Exception {
100100
}
101101
}
102102

103+
@Test
104+
public void permanentCacheEnabled() throws Exception {
105+
App app = new App(AppConfig.builder()
106+
.authTestCacheEnabled(true)
107+
.authTestCacheExpirationMillis(-1L)
108+
.signingSecret(secret)
109+
.singleTeamBotToken("xoxb-1234567890-123456789012-12345678901234567890-permanent")
110+
.slack(slack)
111+
.build());
112+
app.globalShortcut("test-global-shortcut", (req, ctx) -> ctx.ack());
113+
114+
String requestBody = "payload=" + URLEncoder.encode(realPayload, "UTF-8");
115+
116+
Map<String, List<String>> rawHeaders = new HashMap<>();
117+
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
118+
setRequestHeaders(requestBody, rawHeaders, timestamp);
119+
120+
GlobalShortcutRequest req = new GlobalShortcutRequest(requestBody, realPayload, new RequestHeaders(rawHeaders));
121+
assertEquals("seratch", req.getPayload().getUser().getUsername()); // a bit different from message_actions payload
122+
123+
Response response = app.run(req);
124+
assertEquals(200L, response.getStatusCode().longValue());
125+
126+
response = app.run(req);
127+
assertEquals(200L, response.getStatusCode().longValue());
128+
129+
Thread.sleep(300L);
130+
response = app.run(req);
131+
assertEquals(200L, response.getStatusCode().longValue());
132+
133+
Thread.sleep(300L);
134+
response = app.run(req);
135+
assertEquals(200L, response.getStatusCode().longValue());
136+
137+
Thread.sleep(3000L);
138+
response = app.run(req);
139+
assertEquals(200L, response.getStatusCode().longValue());
140+
}
141+
103142
@Test
104143
public void cacheDisabled() throws Exception {
105144
App app = buildApp(false);

0 commit comments

Comments
 (0)