Skip to content

Commit a973bb4

Browse files
authored
Merge pull request #502 from seratch/issue-468
Fix #468 by implementing short-time cache for authorization middleware
2 parents 6cd2da3 + 62c102d commit a973bb4

File tree

5 files changed

+646
-2
lines changed

5 files changed

+646
-2
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,21 @@ public boolean isDistributedApp() {
6969
@Builder.Default
7070
private boolean oAuthCallbackEnabled = false;
7171

72+
73+
/**
74+
* Returns true if auth.test call result cache in SingleTeamAuthorization or MultiTeamsAuthorization middleware
75+
* is enabled. The default is false.
76+
*/
77+
@Builder.Default
78+
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+
*/
84+
@Builder.Default
85+
private long authTestCacheExpirationMillis = 3000L;
86+
7287
@Builder.Default
7388
// https://api.slack.com/authentication/migration
7489
private boolean classicAppPermissionsEnabled = false;

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

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,23 @@
1111
import com.slack.api.bolt.response.Responder;
1212
import com.slack.api.bolt.response.Response;
1313
import com.slack.api.bolt.service.InstallationService;
14+
import com.slack.api.methods.MethodsClient;
1415
import com.slack.api.methods.SlackApiException;
1516
import com.slack.api.methods.response.auth.AuthTestResponse;
1617
import com.slack.api.model.block.LayoutBlock;
18+
import com.slack.api.util.thread.ExecutorServiceFactory;
19+
import lombok.AllArgsConstructor;
20+
import lombok.Data;
1721
import lombok.extern.slf4j.Slf4j;
1822

1923
import java.io.IOException;
2024
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Optional;
27+
import java.util.concurrent.ConcurrentHashMap;
28+
import java.util.concurrent.ConcurrentMap;
29+
import java.util.concurrent.ScheduledExecutorService;
30+
import java.util.concurrent.TimeUnit;
2131

2232
import static com.slack.api.bolt.middleware.MiddlewareOps.isNoAuthRequiredRequest;
2333
import static com.slack.api.bolt.response.ResponseTypes.ephemeral;
@@ -31,6 +41,17 @@ public class MultiTeamsAuthorization implements Middleware {
3141
private final AppConfig config;
3242
private final InstallationService installationService;
3343

44+
@Data
45+
@AllArgsConstructor
46+
static class CachedAuthTestResponse {
47+
private AuthTestResponse response;
48+
private long cachedMillis;
49+
}
50+
51+
// token -> auth.test response
52+
private final ConcurrentMap<String, CachedAuthTestResponse> tokenToAuthTestCache = new ConcurrentHashMap<>();
53+
private final Optional<ScheduledExecutorService> tokenToAuthTestCacheCleaner;
54+
3455
private boolean alwaysRequestUserTokenNeeded;
3556

3657
public boolean isAlwaysRequestUserTokenNeeded() {
@@ -45,6 +66,40 @@ public MultiTeamsAuthorization(AppConfig config, InstallationService installatio
4566
this.config = config;
4667
this.installationService = installationService;
4768
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();
48103
}
49104

50105
@Override
@@ -110,7 +165,7 @@ public Response apply(Request req, Response resp, MiddlewareChain chain) throws
110165

111166
try {
112167
String token = botToken != null ? botToken : userToken;
113-
AuthTestResponse authTestResponse = context.client().authTest(r -> r.token(token));
168+
AuthTestResponse authTestResponse = callAuthTest(token, config, context.client());
114169
if (authTestResponse.isOk()) {
115170
context.setBotToken(botToken);
116171
context.setRequestUserToken(userToken);
@@ -131,6 +186,28 @@ public Response apply(Request req, Response resp, MiddlewareChain chain) throws
131186
}
132187
}
133188

189+
protected AuthTestResponse callAuthTest(String token, AppConfig config, MethodsClient client) throws IOException, SlackApiException {
190+
if (config.isAuthTestCacheEnabled()) {
191+
CachedAuthTestResponse cachedResponse = tokenToAuthTestCache.get(token);
192+
if (cachedResponse != null) {
193+
boolean permanentCacheEnabled = config.getAuthTestCacheExpirationMillis() < 0;
194+
if (permanentCacheEnabled) {
195+
return cachedResponse.getResponse();
196+
}
197+
long millisToExpire = cachedResponse.getCachedMillis() + config.getAuthTestCacheExpirationMillis();
198+
if (millisToExpire > System.currentTimeMillis()) {
199+
return cachedResponse.getResponse();
200+
}
201+
}
202+
AuthTestResponse response = client.authTest(r -> r.token(token));
203+
CachedAuthTestResponse newCache = new CachedAuthTestResponse(response, System.currentTimeMillis());
204+
tokenToAuthTestCache.put(token, newCache);
205+
return response;
206+
} else {
207+
return client.authTest(r -> r.token(token));
208+
}
209+
}
210+
134211
protected Response handleAuthTestError(
135212
String errorCode,
136213
Bot foundBot,

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

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@
88
import com.slack.api.bolt.request.Request;
99
import com.slack.api.bolt.response.Response;
1010
import com.slack.api.bolt.service.InstallationService;
11+
import com.slack.api.methods.MethodsClient;
12+
import com.slack.api.methods.SlackApiException;
1113
import com.slack.api.methods.response.auth.AuthTestResponse;
1214
import lombok.extern.slf4j.Slf4j;
1315

16+
import java.io.IOException;
17+
import java.util.Optional;
18+
import java.util.concurrent.atomic.AtomicLong;
19+
1420
import static com.slack.api.bolt.middleware.MiddlewareOps.isNoAuthRequiredRequest;
1521

1622
/**
@@ -22,6 +28,9 @@ public class SingleTeamAuthorization implements Middleware {
2228
private final AppConfig appConfig;
2329
private final InstallationService installationService;
2430

31+
private Optional<AuthTestResponse> cachedAuthTestResponse = Optional.empty();
32+
private AtomicLong lastCachedMillis = new AtomicLong(0L);
33+
2534
public SingleTeamAuthorization(AppConfig appConfig, InstallationService installationService) {
2635
this.appConfig = appConfig;
2736
this.installationService = installationService;
@@ -34,7 +43,7 @@ public Response apply(Request req, Response resp, MiddlewareChain chain) throws
3443
}
3544

3645
Context context = req.getContext();
37-
AuthTestResponse authResult = context.client().authTest(r -> r.token(appConfig.getSingleTeamBotToken()));
46+
AuthTestResponse authResult = callAuthTest(appConfig, context.client());
3847
if (authResult.isOk()) {
3948
if (context.getBotToken() == null) {
4049
context.setBotToken(appConfig.getSingleTeamBotToken());
@@ -72,4 +81,27 @@ public Response apply(Request req, Response resp, MiddlewareChain chain) throws
7281
.build();
7382
}
7483
}
84+
85+
protected AuthTestResponse callAuthTest(AppConfig config, MethodsClient client) throws IOException, SlackApiException {
86+
if (config.isAuthTestCacheEnabled()) {
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+
}
97+
}
98+
AuthTestResponse response = client.authTest(r -> r.token(config.getSingleTeamBotToken()));
99+
cachedAuthTestResponse = Optional.of(response); // response here is not null for sure
100+
lastCachedMillis.set(System.currentTimeMillis());
101+
return response;
102+
} else {
103+
return client.authTest(r -> r.token(config.getSingleTeamBotToken()));
104+
}
105+
}
106+
75107
}

0 commit comments

Comments
 (0)