Skip to content

Commit 7728153

Browse files
committed
Feature: handle rate limiting of UAA server. Fixes #1307
1 parent 7bae184 commit 7728153

File tree

11 files changed

+298
-7
lines changed

11 files changed

+298
-7
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ Name | Description
297297
`TEST_PROXY_PORT` | _(Optional)_ The port of a proxy to route all requests through. Defaults to `8080`.
298298
`TEST_PROXY_USERNAME` | _(Optional)_ The username for a proxy to route all requests through
299299
`TEST_SKIPSSLVALIDATION` | _(Optional)_ Whether to skip SSL validation when connecting to the Cloud Foundry instance. Defaults to `false`.
300+
`UAA_API_REQUEST_LIMIT` | _(Optional)_ If your UAA server does rate limiting and returns 429 errors, set this variable to the smallest limit configured there. Whether your server limits UAA calls is shown in the log, together with the location of the configuration file on the server. Defaults to `0` (no limit).
300301

301302
If you do not have access to a CloudFoundry instance with admin access, you can run one locally using [bosh-deployment](https://github.com/cloudfoundry/bosh-deployment) & [cf-deployment](https://github.com/cloudfoundry/cf-deployment/) and Virtualbox.
302303

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2013-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.cloudfoundry.reactor.uaa;
18+
19+
import java.util.Map;
20+
import org.cloudfoundry.reactor.ConnectionContext;
21+
import org.cloudfoundry.reactor.TokenProvider;
22+
import org.cloudfoundry.uaa.ratelimit.Ratelimit;
23+
import org.cloudfoundry.uaa.ratelimit.RatelimitRequest;
24+
import org.cloudfoundry.uaa.ratelimit.RatelimitResponse;
25+
import reactor.core.publisher.Mono;
26+
27+
public final class ReactorRatelimit extends AbstractUaaOperations implements Ratelimit {
28+
29+
/**
30+
* Creates an instance
31+
*
32+
* @param connectionContext the {@link ConnectionContext} to use when communicating with the server
33+
* @param root the root URI of the server. Typically something like {@code https://uaa.run.pivotal.io}.
34+
* @param tokenProvider the {@link TokenProvider} to use when communicating with the server
35+
* @param requestTags map with custom http headers which will be added to web request
36+
*/
37+
public ReactorRatelimit(
38+
ConnectionContext connectionContext,
39+
Mono<String> root,
40+
TokenProvider tokenProvider,
41+
Map<String, String> requestTags) {
42+
super(connectionContext, root, tokenProvider, requestTags);
43+
}
44+
45+
@Override
46+
public Mono<RatelimitResponse> getRatelimit(RatelimitRequest request) {
47+
return get(
48+
request,
49+
RatelimitResponse.class,
50+
builder -> builder.pathSegment("RateLimitingStatus"))
51+
.checkpoint();
52+
}
53+
}

cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/_ReactorUaaClient.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.cloudfoundry.uaa.groups.Groups;
3434
import org.cloudfoundry.uaa.identityproviders.IdentityProviders;
3535
import org.cloudfoundry.uaa.identityzones.IdentityZones;
36+
import org.cloudfoundry.uaa.ratelimit.Ratelimit;
3637
import org.cloudfoundry.uaa.serverinformation.ServerInformation;
3738
import org.cloudfoundry.uaa.tokens.Tokens;
3839
import org.cloudfoundry.uaa.users.Users;
@@ -104,6 +105,12 @@ public Users users() {
104105
return new ReactorUsers(getConnectionContext(), getRoot(), getTokenProvider(), getRequestTags());
105106
}
106107

108+
@Override
109+
@Value.Derived
110+
public Ratelimit rateLimit() {
111+
return new ReactorRatelimit(getConnectionContext(), getRoot(), getTokenProvider(), getRequestTags());
112+
}
113+
107114
/**
108115
* The connection context
109116
*/

cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceInstances/ReactorServiceInstancesV3Test.java renamed to cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceinstances/ReactorServiceInstancesV3Test.java

File renamed without changes.

cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/UaaClient.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.cloudfoundry.uaa.groups.Groups;
2222
import org.cloudfoundry.uaa.identityproviders.IdentityProviders;
2323
import org.cloudfoundry.uaa.identityzones.IdentityZones;
24+
import org.cloudfoundry.uaa.ratelimit.Ratelimit;
2425
import org.cloudfoundry.uaa.serverinformation.ServerInformation;
2526
import org.cloudfoundry.uaa.tokens.Tokens;
2627
import org.cloudfoundry.uaa.users.Users;
@@ -80,4 +81,9 @@ public interface UaaClient {
8081
* Main entry point to the UAA User Client API
8182
*/
8283
Users users();
84+
85+
/**
86+
* Main entry point to the UAA Ratelimit API
87+
*/
88+
Ratelimit rateLimit();
8389
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2013-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.cloudfoundry.uaa.ratelimit;
18+
19+
import reactor.core.publisher.Mono;
20+
21+
/**
22+
* Main entry point to the UAA Ratelimit Client API
23+
*/
24+
public interface Ratelimit {
25+
26+
Mono<RatelimitResponse> getRatelimit(RatelimitRequest request);
27+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2013-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.cloudfoundry.uaa.ratelimit;
18+
19+
20+
import com.fasterxml.jackson.annotation.JsonProperty;
21+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
22+
23+
import java.util.Date;
24+
25+
import org.immutables.value.Value;
26+
27+
/**
28+
* The payload for the uaa ratelimiting
29+
*/
30+
@JsonDeserialize
31+
@Value.Immutable
32+
abstract class _Current {
33+
34+
/**
35+
* The number of configured limiter mappings
36+
*/
37+
@JsonProperty("limiterMappings")
38+
abstract Integer getLimiterMappings();
39+
40+
/**
41+
* Is ratelimit "ACTIVE" or not? Possible values are DISABLED, PENDING, ACTIVE
42+
*/
43+
@JsonProperty("status")
44+
abstract String getStatus();
45+
46+
/**
47+
* Timestamp, when this Current was created.
48+
*/
49+
@JsonProperty("asOf")
50+
abstract Date getTimeOfCurrent();
51+
52+
/**
53+
* The credentialIdExtractor
54+
*/
55+
@JsonProperty("credentialIdExtractor")
56+
abstract String getCredentialIdExtractor();
57+
58+
/**
59+
* The loggingLevel. Valid values include: "OnlyLimited", "AllCalls" and "AllCallsWithDetails"
60+
*/
61+
@JsonProperty("loggingLevel")
62+
abstract String getLoggingLevel();
63+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2013-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.cloudfoundry.uaa.ratelimit;
18+
19+
import org.immutables.value.Value;
20+
21+
@Value.Immutable
22+
abstract class _RatelimitRequest {
23+
24+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2013-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.cloudfoundry.uaa.ratelimit;
18+
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
21+
import org.cloudfoundry.Nullable;
22+
import org.immutables.value.Value;
23+
24+
@JsonDeserialize
25+
@Value.Immutable
26+
abstract class _RatelimitResponse {
27+
28+
@JsonProperty("current")
29+
@Nullable
30+
abstract Current getCurrentData();
31+
32+
@JsonProperty("fromSource")
33+
@Nullable
34+
abstract String getFromSource();
35+
36+
}

integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
import org.cloudfoundry.uaa.groups.ListGroupsRequest;
7474
import org.cloudfoundry.uaa.groups.ListGroupsResponse;
7575
import org.cloudfoundry.uaa.groups.MemberType;
76+
import org.cloudfoundry.uaa.ratelimit.Current;
77+
import org.cloudfoundry.uaa.ratelimit.RatelimitRequest;
78+
import org.cloudfoundry.uaa.ratelimit.RatelimitResponse;
7679
import org.cloudfoundry.uaa.users.CreateUserRequest;
7780
import org.cloudfoundry.uaa.users.CreateUserResponse;
7881
import org.cloudfoundry.uaa.users.Email;
@@ -195,7 +198,8 @@ NetworkingClient adminNetworkingClient(
195198
UaaClient adminUaaClient(
196199
ConnectionContext connectionContext,
197200
@Value("${test.admin.clientId}") String clientId,
198-
@Value("${test.admin.clientSecret}") String clientSecret) {
201+
@Value("${test.admin.clientSecret}") String clientSecret,
202+
int uaaLimiterMapping) {
199203
return new ThrottlingUaaClient(
200204
ReactorUaaClient.builder()
201205
.connectionContext(connectionContext)
@@ -204,7 +208,8 @@ UaaClient adminUaaClient(
204208
.clientId(clientId)
205209
.clientSecret(clientSecret)
206210
.build())
207-
.build());
211+
.build(),
212+
uaaLimiterMapping);
208213
}
209214

210215
@Bean(initMethod = "block")
@@ -244,6 +249,7 @@ String clientSecret(NameFactory nameFactory) {
244249
}
245250

246251
@Bean
252+
@DependsOn("uaaRatelimit")
247253
CloudFoundryCleaner cloudFoundryCleaner(
248254
@Qualifier("admin") CloudFoundryClient cloudFoundryClient,
249255
NameFactory nameFactory,
@@ -320,6 +326,54 @@ DefaultConnectionContext connectionContext(
320326
return connectionContext.build();
321327
}
322328

329+
@Bean
330+
public Integer uaaLimiterMapping(
331+
@Value("${uaa.api.request.limit:#{null}}") Integer environmentRequestLimit) {
332+
return environmentRequestLimit;
333+
}
334+
335+
@Bean
336+
Boolean uaaRatelimit(
337+
ConnectionContext connectionContext, @Qualifier("admin") UaaClient uaaClient) {
338+
return uaaClient
339+
.rateLimit()
340+
.getRatelimit(RatelimitRequest.builder().build())
341+
.map(response -> getServerRatelimit(response))
342+
.timeout(Duration.ofSeconds(5))
343+
.onErrorResume(
344+
ex -> {
345+
logger.error(
346+
"Warning: could not fetch UAA rate limit, using default"
347+
+ " "
348+
+ 0
349+
+ ". Cause: "
350+
+ ex);
351+
return Mono.just(false);
352+
})
353+
.block();
354+
}
355+
356+
private Boolean getServerRatelimit(RatelimitResponse response) {
357+
Current curr = response.getCurrentData();
358+
if (!"ACTIVE".equals(curr.getStatus())) {
359+
logger.debug(
360+
"UaaRatelimitInitializer server ratelimit is not 'ACTIVE', but "
361+
+ curr.getStatus()
362+
+ ". Ignoring server value for ratelimit.");
363+
return false;
364+
}
365+
Integer result = curr.getLimiterMappings();
366+
logger.info(
367+
"Server uses uaa rate limiting. There are "
368+
+ result
369+
+ " mappings declared in file "
370+
+ response.getFromSource());
371+
logger.info(
372+
"If you encounter 429 return codes, configure uaa rate limiting or set variable"
373+
+ " 'UAA_API_REQUEST_LIMIT' to a save value.");
374+
return true;
375+
}
376+
323377
@Bean
324378
DopplerClient dopplerClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
325379
return ReactorDopplerClient.builder()
@@ -644,12 +698,16 @@ PasswordGrantTokenProvider tokenProvider(
644698
}
645699

646700
@Bean
647-
UaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
701+
UaaClient uaaClient(
702+
ConnectionContext connectionContext,
703+
TokenProvider tokenProvider,
704+
int uaaLimiterMapping) {
648705
return new ThrottlingUaaClient(
649706
ReactorUaaClient.builder()
650707
.connectionContext(connectionContext)
651708
.tokenProvider(tokenProvider)
652-
.build());
709+
.build(),
710+
uaaLimiterMapping);
653711
}
654712

655713
@Bean(initMethod = "block")

0 commit comments

Comments
 (0)