From 7bae184892f7d84129a258666547f740e52696fb Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 2 Dec 2025 17:17:05 +0100 Subject: [PATCH 1/3] poc uaa throttling --- .../reactor/util/RequestLogger.java | 2 +- integration-test/pom.xml | 10 + .../IntegrationTestConfiguration.java | 30 +- .../org/cloudfoundry/ThrottlingUaaClient.java | 321 ++++++++++++++++++ .../src/test/resources/logback-test.xml | 2 +- 5 files changed, 349 insertions(+), 16 deletions(-) create mode 100644 integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java index d0f3cc5ae3..4c94223c57 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java @@ -34,7 +34,7 @@ public class RequestLogger { private long requestSentTime; public void request(HttpClientRequest request) { - request(String.format("%-6s {}", request.method()), request.uri()); + request(String.format("%-6s {}", request.method()), request.resourceUrl()); } public void response(HttpClientResponse response) { diff --git a/integration-test/pom.xml b/integration-test/pom.xml index c3433feced..32a5dc3c8c 100644 --- a/integration-test/pom.xml +++ b/integration-test/pom.xml @@ -70,6 +70,16 @@ ${project.version} test + + io.github.resilience4j + resilience4j-ratelimiter + 1.7.0 + + + io.github.resilience4j + resilience4j-reactor + 1.7.0 + org.cloudfoundry cloudfoundry-util diff --git a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java index c0b3f44b30..5d1062f9fd 100644 --- a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java +++ b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java @@ -192,18 +192,19 @@ NetworkingClient adminNetworkingClient( @Bean @Qualifier("admin") - ReactorUaaClient adminUaaClient( + UaaClient adminUaaClient( ConnectionContext connectionContext, @Value("${test.admin.clientId}") String clientId, @Value("${test.admin.clientSecret}") String clientSecret) { - return ReactorUaaClient.builder() - .connectionContext(connectionContext) - .tokenProvider( - ClientCredentialsGrantTokenProvider.builder() - .clientId(clientId) - .clientSecret(clientSecret) - .build()) - .build(); + return new ThrottlingUaaClient( + ReactorUaaClient.builder() + .connectionContext(connectionContext) + .tokenProvider( + ClientCredentialsGrantTokenProvider.builder() + .clientId(clientId) + .clientSecret(clientSecret) + .build()) + .build()); } @Bean(initMethod = "block") @@ -643,11 +644,12 @@ PasswordGrantTokenProvider tokenProvider( } @Bean - ReactorUaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) { - return ReactorUaaClient.builder() - .connectionContext(connectionContext) - .tokenProvider(tokenProvider) - .build(); + UaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) { + return new ThrottlingUaaClient( + ReactorUaaClient.builder() + .connectionContext(connectionContext) + .tokenProvider(tokenProvider) + .build()); } @Bean(initMethod = "block") diff --git a/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java b/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java new file mode 100644 index 0000000000..0d8c6de7c9 --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java @@ -0,0 +1,321 @@ +package org.cloudfoundry; + +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.github.resilience4j.ratelimiter.RateLimiterConfig; +import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator; +import java.time.Duration; +import org.cloudfoundry.uaa.UaaClient; +import org.cloudfoundry.uaa.authorizations.Authorizations; +import org.cloudfoundry.uaa.clients.Clients; +import org.cloudfoundry.uaa.groups.AddMemberRequest; +import org.cloudfoundry.uaa.groups.AddMemberResponse; +import org.cloudfoundry.uaa.groups.CheckMembershipRequest; +import org.cloudfoundry.uaa.groups.CheckMembershipResponse; +import org.cloudfoundry.uaa.groups.CreateGroupRequest; +import org.cloudfoundry.uaa.groups.CreateGroupResponse; +import org.cloudfoundry.uaa.groups.DeleteGroupRequest; +import org.cloudfoundry.uaa.groups.DeleteGroupResponse; +import org.cloudfoundry.uaa.groups.GetGroupRequest; +import org.cloudfoundry.uaa.groups.GetGroupResponse; +import org.cloudfoundry.uaa.groups.Groups; +import org.cloudfoundry.uaa.groups.ListExternalGroupMappingsRequest; +import org.cloudfoundry.uaa.groups.ListExternalGroupMappingsResponse; +import org.cloudfoundry.uaa.groups.ListGroupsRequest; +import org.cloudfoundry.uaa.groups.ListGroupsResponse; +import org.cloudfoundry.uaa.groups.ListMembersRequest; +import org.cloudfoundry.uaa.groups.ListMembersResponse; +import org.cloudfoundry.uaa.groups.MapExternalGroupRequest; +import org.cloudfoundry.uaa.groups.MapExternalGroupResponse; +import org.cloudfoundry.uaa.groups.RemoveMemberRequest; +import org.cloudfoundry.uaa.groups.RemoveMemberResponse; +import org.cloudfoundry.uaa.groups.UnmapExternalGroupByGroupDisplayNameRequest; +import org.cloudfoundry.uaa.groups.UnmapExternalGroupByGroupDisplayNameResponse; +import org.cloudfoundry.uaa.groups.UnmapExternalGroupByGroupIdRequest; +import org.cloudfoundry.uaa.groups.UnmapExternalGroupByGroupIdResponse; +import org.cloudfoundry.uaa.groups.UpdateGroupRequest; +import org.cloudfoundry.uaa.groups.UpdateGroupResponse; +import org.cloudfoundry.uaa.identityproviders.IdentityProviders; +import org.cloudfoundry.uaa.identityzones.IdentityZones; +import org.cloudfoundry.uaa.serverinformation.ServerInformation; +import org.cloudfoundry.uaa.tokens.Tokens; +import org.cloudfoundry.uaa.users.ChangeUserPasswordRequest; +import org.cloudfoundry.uaa.users.ChangeUserPasswordResponse; +import org.cloudfoundry.uaa.users.CreateUserRequest; +import org.cloudfoundry.uaa.users.CreateUserResponse; +import org.cloudfoundry.uaa.users.DeleteUserRequest; +import org.cloudfoundry.uaa.users.DeleteUserResponse; +import org.cloudfoundry.uaa.users.ExpirePasswordRequest; +import org.cloudfoundry.uaa.users.ExpirePasswordResponse; +import org.cloudfoundry.uaa.users.GetUserVerificationLinkRequest; +import org.cloudfoundry.uaa.users.GetUserVerificationLinkResponse; +import org.cloudfoundry.uaa.users.InviteUsersRequest; +import org.cloudfoundry.uaa.users.InviteUsersResponse; +import org.cloudfoundry.uaa.users.ListUsersRequest; +import org.cloudfoundry.uaa.users.ListUsersResponse; +import org.cloudfoundry.uaa.users.LookupUserIdsRequest; +import org.cloudfoundry.uaa.users.LookupUserIdsResponse; +import org.cloudfoundry.uaa.users.UpdateUserRequest; +import org.cloudfoundry.uaa.users.UpdateUserResponse; +import org.cloudfoundry.uaa.users.UserInfoRequest; +import org.cloudfoundry.uaa.users.UserInfoResponse; +import org.cloudfoundry.uaa.users.Users; +import org.cloudfoundry.uaa.users.VerifyUserRequest; +import org.cloudfoundry.uaa.users.VerifyUserResponse; +import reactor.core.publisher.Mono; + +public class ThrottlingUaaClient implements UaaClient { + + private final UaaClient delegate; + + private final RateLimiter rateLimiter; + private final ThrottledUsers users; + private Groups groups; + + public ThrottlingUaaClient(UaaClient delegate) { + this.delegate = delegate; + RateLimiterConfig config = + RateLimiterConfig.custom() + .limitForPeriod(5) + .limitRefreshPeriod(Duration.ofSeconds(1)) + .build(); + this.rateLimiter = RateLimiter.of("uaa", config); + this.users = new ThrottledUsers(); + this.groups = new ThrottledGroups(); + } + + @Override + public Authorizations authorizations() { + return this.delegate.authorizations(); + } + + @Override + public Clients clients() { + return this.delegate.clients(); + } + + @Override + public Mono getUsername() { + return this.delegate.getUsername(); + } + + @Override + public IdentityProviders identityProviders() { + return this.delegate.identityProviders(); + } + + @Override + public IdentityZones identityZones() { + return this.delegate.identityZones(); + } + + @Override + public ServerInformation serverInformation() { + return this.delegate.serverInformation(); + } + + @Override + public Tokens tokens() { + return this.delegate.tokens(); + } + + @Override + public Users users() { + return users; + } + + @Override + public Groups groups() { + return groups; + } + + public class ThrottledUsers implements Users { + + private final Users usersDelegate; + + public ThrottledUsers() { + this.usersDelegate = delegate.users(); + } + + @Override + public Mono changePassword(ChangeUserPasswordRequest request) { + return this.usersDelegate + .changePassword(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono create(CreateUserRequest request) { + return this.usersDelegate + .create(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono delete(DeleteUserRequest request) { + return this.usersDelegate + .delete(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono expirePassword(ExpirePasswordRequest request) { + return this.usersDelegate + .expirePassword(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono getVerificationLink( + GetUserVerificationLinkRequest request) { + return this.usersDelegate + .getVerificationLink(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono invite(InviteUsersRequest request) { + return this.usersDelegate + .invite(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono list(ListUsersRequest request) { + return this.usersDelegate + .list(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono lookup(LookupUserIdsRequest request) { + return this.usersDelegate + .lookup(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono update(UpdateUserRequest request) { + return this.usersDelegate + .update(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono userInfo(UserInfoRequest request) { + return this.usersDelegate + .userInfo(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono verify(VerifyUserRequest request) { + return this.usersDelegate + .verify(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + } + + public class ThrottledGroups implements Groups { + + public final Groups groupsDelegate; + + public ThrottledGroups() { + this.groupsDelegate = delegate.groups(); + } + + @Override + public Mono addMember(AddMemberRequest request) { + return this.groupsDelegate + .addMember(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono checkMembership(CheckMembershipRequest request) { + return this.groupsDelegate + .checkMembership(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono create(CreateGroupRequest request) { + return this.groupsDelegate + .create(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono delete(DeleteGroupRequest request) { + return this.groupsDelegate + .delete(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono get(GetGroupRequest request) { + return this.groupsDelegate + .get(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono list(ListGroupsRequest request) { + return this.groupsDelegate + .list(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono listExternalGroupMappings( + ListExternalGroupMappingsRequest request) { + return this.groupsDelegate + .listExternalGroupMappings(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono listMembers(ListMembersRequest request) { + return this.groupsDelegate + .listMembers(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono mapExternalGroup(MapExternalGroupRequest request) { + return this.groupsDelegate + .mapExternalGroup(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono removeMember(RemoveMemberRequest request) { + return this.groupsDelegate + .removeMember(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono + unmapExternalGroupByGroupDisplayName( + UnmapExternalGroupByGroupDisplayNameRequest request) { + return this.groupsDelegate + .unmapExternalGroupByGroupDisplayName(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono unmapExternalGroupByGroupId( + UnmapExternalGroupByGroupIdRequest request) { + return this.groupsDelegate + .unmapExternalGroupByGroupId(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono update(UpdateGroupRequest request) { + return this.groupsDelegate + .update(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + } +} diff --git a/integration-test/src/test/resources/logback-test.xml b/integration-test/src/test/resources/logback-test.xml index 4d9869fc6e..0872bbe5aa 100644 --- a/integration-test/src/test/resources/logback-test.xml +++ b/integration-test/src/test/resources/logback-test.xml @@ -26,7 +26,7 @@ - + From 7728153030edaa67bf5cce6f9a37c6dea2798ede Mon Sep 17 00:00:00 2001 From: Georg Lokowandt Date: Wed, 17 Dec 2025 19:37:57 +0100 Subject: [PATCH 2/3] Feature: handle rate limiting of UAA server. Fixes #1307 --- README.md | 1 + .../reactor/uaa/ReactorRatelimit.java | 53 +++++++++++++++ .../reactor/uaa/_ReactorUaaClient.java | 7 ++ .../ReactorServiceInstancesV3Test.java | 0 .../java/org/cloudfoundry/uaa/UaaClient.java | 6 ++ .../cloudfoundry/uaa/ratelimit/Ratelimit.java | 27 ++++++++ .../cloudfoundry/uaa/ratelimit/_Current.java | 63 ++++++++++++++++++ .../uaa/ratelimit/_RatelimitRequest.java | 24 +++++++ .../uaa/ratelimit/_RatelimitResponse.java | 36 ++++++++++ .../IntegrationTestConfiguration.java | 66 +++++++++++++++++-- .../org/cloudfoundry/ThrottlingUaaClient.java | 22 ++++++- 11 files changed, 298 insertions(+), 7 deletions(-) create mode 100644 cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/ReactorRatelimit.java rename cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/{serviceInstances => serviceinstances}/ReactorServiceInstancesV3Test.java (100%) create mode 100644 cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/Ratelimit.java create mode 100644 cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_Current.java create mode 100644 cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitRequest.java create mode 100644 cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitResponse.java diff --git a/README.md b/README.md index ffed2f36f4..6f6e77f4ae 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,7 @@ Name | Description `TEST_PROXY_PORT` | _(Optional)_ The port of a proxy to route all requests through. Defaults to `8080`. `TEST_PROXY_USERNAME` | _(Optional)_ The username for a proxy to route all requests through `TEST_SKIPSSLVALIDATION` | _(Optional)_ Whether to skip SSL validation when connecting to the Cloud Foundry instance. Defaults to `false`. +`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). 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. diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/ReactorRatelimit.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/ReactorRatelimit.java new file mode 100644 index 0000000000..c350367d61 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/ReactorRatelimit.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.reactor.uaa; + +import java.util.Map; +import org.cloudfoundry.reactor.ConnectionContext; +import org.cloudfoundry.reactor.TokenProvider; +import org.cloudfoundry.uaa.ratelimit.Ratelimit; +import org.cloudfoundry.uaa.ratelimit.RatelimitRequest; +import org.cloudfoundry.uaa.ratelimit.RatelimitResponse; +import reactor.core.publisher.Mono; + +public final class ReactorRatelimit extends AbstractUaaOperations implements Ratelimit { + + /** + * Creates an instance + * + * @param connectionContext the {@link ConnectionContext} to use when communicating with the server + * @param root the root URI of the server. Typically something like {@code https://uaa.run.pivotal.io}. + * @param tokenProvider the {@link TokenProvider} to use when communicating with the server + * @param requestTags map with custom http headers which will be added to web request + */ + public ReactorRatelimit( + ConnectionContext connectionContext, + Mono root, + TokenProvider tokenProvider, + Map requestTags) { + super(connectionContext, root, tokenProvider, requestTags); + } + + @Override + public Mono getRatelimit(RatelimitRequest request) { + return get( + request, + RatelimitResponse.class, + builder -> builder.pathSegment("RateLimitingStatus")) + .checkpoint(); + } +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/_ReactorUaaClient.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/_ReactorUaaClient.java index 1b2d613c4e..a1b28ca848 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/_ReactorUaaClient.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/_ReactorUaaClient.java @@ -33,6 +33,7 @@ import org.cloudfoundry.uaa.groups.Groups; import org.cloudfoundry.uaa.identityproviders.IdentityProviders; import org.cloudfoundry.uaa.identityzones.IdentityZones; +import org.cloudfoundry.uaa.ratelimit.Ratelimit; import org.cloudfoundry.uaa.serverinformation.ServerInformation; import org.cloudfoundry.uaa.tokens.Tokens; import org.cloudfoundry.uaa.users.Users; @@ -104,6 +105,12 @@ public Users users() { return new ReactorUsers(getConnectionContext(), getRoot(), getTokenProvider(), getRequestTags()); } + @Override + @Value.Derived + public Ratelimit rateLimit() { + return new ReactorRatelimit(getConnectionContext(), getRoot(), getTokenProvider(), getRequestTags()); + } + /** * The connection context */ diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceInstances/ReactorServiceInstancesV3Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceinstances/ReactorServiceInstancesV3Test.java similarity index 100% rename from cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceInstances/ReactorServiceInstancesV3Test.java rename to cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceinstances/ReactorServiceInstancesV3Test.java diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/UaaClient.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/UaaClient.java index 9c88d37f59..665fb920ee 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/UaaClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/UaaClient.java @@ -21,6 +21,7 @@ import org.cloudfoundry.uaa.groups.Groups; import org.cloudfoundry.uaa.identityproviders.IdentityProviders; import org.cloudfoundry.uaa.identityzones.IdentityZones; +import org.cloudfoundry.uaa.ratelimit.Ratelimit; import org.cloudfoundry.uaa.serverinformation.ServerInformation; import org.cloudfoundry.uaa.tokens.Tokens; import org.cloudfoundry.uaa.users.Users; @@ -80,4 +81,9 @@ public interface UaaClient { * Main entry point to the UAA User Client API */ Users users(); + + /** + * Main entry point to the UAA Ratelimit API + */ + Ratelimit rateLimit(); } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/Ratelimit.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/Ratelimit.java new file mode 100644 index 0000000000..c277fa7331 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/Ratelimit.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.uaa.ratelimit; + +import reactor.core.publisher.Mono; + +/** + * Main entry point to the UAA Ratelimit Client API + */ +public interface Ratelimit { + + Mono getRatelimit(RatelimitRequest request); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_Current.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_Current.java new file mode 100644 index 0000000000..57811e9e59 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_Current.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.uaa.ratelimit; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.Date; + +import org.immutables.value.Value; + +/** + * The payload for the uaa ratelimiting + */ +@JsonDeserialize +@Value.Immutable +abstract class _Current { + + /** + * The number of configured limiter mappings + */ + @JsonProperty("limiterMappings") + abstract Integer getLimiterMappings(); + + /** + * Is ratelimit "ACTIVE" or not? Possible values are DISABLED, PENDING, ACTIVE + */ + @JsonProperty("status") + abstract String getStatus(); + + /** + * Timestamp, when this Current was created. + */ + @JsonProperty("asOf") + abstract Date getTimeOfCurrent(); + + /** + * The credentialIdExtractor + */ + @JsonProperty("credentialIdExtractor") + abstract String getCredentialIdExtractor(); + + /** + * The loggingLevel. Valid values include: "OnlyLimited", "AllCalls" and "AllCallsWithDetails" + */ + @JsonProperty("loggingLevel") + abstract String getLoggingLevel(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitRequest.java new file mode 100644 index 0000000000..be75d58bfa --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitRequest.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.uaa.ratelimit; + +import org.immutables.value.Value; + +@Value.Immutable +abstract class _RatelimitRequest { + +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitResponse.java new file mode 100644 index 0000000000..0fe927cbf6 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitResponse.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.uaa.ratelimit; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.cloudfoundry.Nullable; +import org.immutables.value.Value; + +@JsonDeserialize +@Value.Immutable +abstract class _RatelimitResponse { + + @JsonProperty("current") + @Nullable + abstract Current getCurrentData(); + + @JsonProperty("fromSource") + @Nullable + abstract String getFromSource(); + +} diff --git a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java index 5d1062f9fd..18f9fae750 100644 --- a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java +++ b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java @@ -73,6 +73,9 @@ import org.cloudfoundry.uaa.groups.ListGroupsRequest; import org.cloudfoundry.uaa.groups.ListGroupsResponse; import org.cloudfoundry.uaa.groups.MemberType; +import org.cloudfoundry.uaa.ratelimit.Current; +import org.cloudfoundry.uaa.ratelimit.RatelimitRequest; +import org.cloudfoundry.uaa.ratelimit.RatelimitResponse; import org.cloudfoundry.uaa.users.CreateUserRequest; import org.cloudfoundry.uaa.users.CreateUserResponse; import org.cloudfoundry.uaa.users.Email; @@ -195,7 +198,8 @@ NetworkingClient adminNetworkingClient( UaaClient adminUaaClient( ConnectionContext connectionContext, @Value("${test.admin.clientId}") String clientId, - @Value("${test.admin.clientSecret}") String clientSecret) { + @Value("${test.admin.clientSecret}") String clientSecret, + int uaaLimiterMapping) { return new ThrottlingUaaClient( ReactorUaaClient.builder() .connectionContext(connectionContext) @@ -204,7 +208,8 @@ UaaClient adminUaaClient( .clientId(clientId) .clientSecret(clientSecret) .build()) - .build()); + .build(), + uaaLimiterMapping); } @Bean(initMethod = "block") @@ -244,6 +249,7 @@ String clientSecret(NameFactory nameFactory) { } @Bean + @DependsOn("uaaRatelimit") CloudFoundryCleaner cloudFoundryCleaner( @Qualifier("admin") CloudFoundryClient cloudFoundryClient, NameFactory nameFactory, @@ -320,6 +326,54 @@ DefaultConnectionContext connectionContext( return connectionContext.build(); } + @Bean + public Integer uaaLimiterMapping( + @Value("${uaa.api.request.limit:#{null}}") Integer environmentRequestLimit) { + return environmentRequestLimit; + } + + @Bean + Boolean uaaRatelimit( + ConnectionContext connectionContext, @Qualifier("admin") UaaClient uaaClient) { + return uaaClient + .rateLimit() + .getRatelimit(RatelimitRequest.builder().build()) + .map(response -> getServerRatelimit(response)) + .timeout(Duration.ofSeconds(5)) + .onErrorResume( + ex -> { + logger.error( + "Warning: could not fetch UAA rate limit, using default" + + " " + + 0 + + ". Cause: " + + ex); + return Mono.just(false); + }) + .block(); + } + + private Boolean getServerRatelimit(RatelimitResponse response) { + Current curr = response.getCurrentData(); + if (!"ACTIVE".equals(curr.getStatus())) { + logger.debug( + "UaaRatelimitInitializer server ratelimit is not 'ACTIVE', but " + + curr.getStatus() + + ". Ignoring server value for ratelimit."); + return false; + } + Integer result = curr.getLimiterMappings(); + logger.info( + "Server uses uaa rate limiting. There are " + + result + + " mappings declared in file " + + response.getFromSource()); + logger.info( + "If you encounter 429 return codes, configure uaa rate limiting or set variable" + + " 'UAA_API_REQUEST_LIMIT' to a save value."); + return true; + } + @Bean DopplerClient dopplerClient(ConnectionContext connectionContext, TokenProvider tokenProvider) { return ReactorDopplerClient.builder() @@ -644,12 +698,16 @@ PasswordGrantTokenProvider tokenProvider( } @Bean - UaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) { + UaaClient uaaClient( + ConnectionContext connectionContext, + TokenProvider tokenProvider, + int uaaLimiterMapping) { return new ThrottlingUaaClient( ReactorUaaClient.builder() .connectionContext(connectionContext) .tokenProvider(tokenProvider) - .build()); + .build(), + uaaLimiterMapping); } @Bean(initMethod = "block") diff --git a/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java b/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java index 0d8c6de7c9..97923e2d73 100644 --- a/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java +++ b/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java @@ -4,6 +4,7 @@ import io.github.resilience4j.ratelimiter.RateLimiterConfig; import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator; import java.time.Duration; +import org.cloudfoundry.reactor.uaa.ReactorUaaClient; import org.cloudfoundry.uaa.UaaClient; import org.cloudfoundry.uaa.authorizations.Authorizations; import org.cloudfoundry.uaa.clients.Clients; @@ -36,6 +37,7 @@ import org.cloudfoundry.uaa.groups.UpdateGroupResponse; import org.cloudfoundry.uaa.identityproviders.IdentityProviders; import org.cloudfoundry.uaa.identityzones.IdentityZones; +import org.cloudfoundry.uaa.ratelimit.Ratelimit; import org.cloudfoundry.uaa.serverinformation.ServerInformation; import org.cloudfoundry.uaa.tokens.Tokens; import org.cloudfoundry.uaa.users.ChangeUserPasswordRequest; @@ -61,6 +63,7 @@ import org.cloudfoundry.uaa.users.Users; import org.cloudfoundry.uaa.users.VerifyUserRequest; import org.cloudfoundry.uaa.users.VerifyUserResponse; +import org.immutables.value.Value; import reactor.core.publisher.Mono; public class ThrottlingUaaClient implements UaaClient { @@ -71,14 +74,21 @@ public class ThrottlingUaaClient implements UaaClient { private final ThrottledUsers users; private Groups groups; - public ThrottlingUaaClient(UaaClient delegate) { + public ThrottlingUaaClient(ReactorUaaClient delegate, int uaaLimit) { + // uaaLimit is calls per second. We need the milliseconds for one call because + // resilience4j uses sliced timeslots, while the uaa server uses a sliding window. + int timeBasePerRequest = 1000 / uaaLimit; this.delegate = delegate; RateLimiterConfig config = RateLimiterConfig.custom() - .limitForPeriod(5) - .limitRefreshPeriod(Duration.ofSeconds(1)) + .limitForPeriod(1) + .limitRefreshPeriod( + Duration.ofMillis( + timeBasePerRequest + 20)) // 20 ms to handle clock skew. + .timeoutDuration(Duration.ofSeconds(10)) .build(); this.rateLimiter = RateLimiter.of("uaa", config); + this.users = new ThrottledUsers(); this.groups = new ThrottledGroups(); } @@ -128,6 +138,12 @@ public Groups groups() { return groups; } + @Override + @Value.Derived + public Ratelimit rateLimit() { + return this.delegate.rateLimit(); + } + public class ThrottledUsers implements Users { private final Users usersDelegate; From 5a2db7b12cd15bd4198df57a4fe5864c5857f0a8 Mon Sep 17 00:00:00 2001 From: Georg Lokowandt Date: Tue, 23 Dec 2025 13:26:42 +0100 Subject: [PATCH 3/3] fix review findings --- integration-test/pom.xml | 4 +- .../IntegrationTestConfiguration.java | 78 ++++--------------- .../org/cloudfoundry/ThrottlingUaaClient.java | 23 ++++-- .../uaa/RatelimitTestConfiguration.java | 73 +++++++++++++++++ .../cloudfoundry/uaa/UaaRatelimitTest.java | 72 +++++++++++++++++ .../src/test/resources/logback-test.xml | 3 +- pom.xml | 1 + 7 files changed, 184 insertions(+), 70 deletions(-) create mode 100644 integration-test/src/test/java/org/cloudfoundry/uaa/RatelimitTestConfiguration.java create mode 100644 integration-test/src/test/java/org/cloudfoundry/uaa/UaaRatelimitTest.java diff --git a/integration-test/pom.xml b/integration-test/pom.xml index 32a5dc3c8c..fbad53c625 100644 --- a/integration-test/pom.xml +++ b/integration-test/pom.xml @@ -73,12 +73,12 @@ io.github.resilience4j resilience4j-ratelimiter - 1.7.0 + ${resilience4j.version} io.github.resilience4j resilience4j-reactor - 1.7.0 + ${resilience4j.version} org.cloudfoundry diff --git a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java index 18f9fae750..36c30c3578 100644 --- a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java +++ b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java @@ -73,9 +73,6 @@ import org.cloudfoundry.uaa.groups.ListGroupsRequest; import org.cloudfoundry.uaa.groups.ListGroupsResponse; import org.cloudfoundry.uaa.groups.MemberType; -import org.cloudfoundry.uaa.ratelimit.Current; -import org.cloudfoundry.uaa.ratelimit.RatelimitRequest; -import org.cloudfoundry.uaa.ratelimit.RatelimitResponse; import org.cloudfoundry.uaa.users.CreateUserRequest; import org.cloudfoundry.uaa.users.CreateUserResponse; import org.cloudfoundry.uaa.users.Email; @@ -199,8 +196,8 @@ UaaClient adminUaaClient( ConnectionContext connectionContext, @Value("${test.admin.clientId}") String clientId, @Value("${test.admin.clientSecret}") String clientSecret, - int uaaLimiterMapping) { - return new ThrottlingUaaClient( + @Value("${uaa.api.request.limit:#{null}}") Integer environmentRequestLimit) { + ReactorUaaClient unthrottledClient = ReactorUaaClient.builder() .connectionContext(connectionContext) .tokenProvider( @@ -208,8 +205,12 @@ UaaClient adminUaaClient( .clientId(clientId) .clientSecret(clientSecret) .build()) - .build(), - uaaLimiterMapping); + .build(); + if (environmentRequestLimit == null) { + return unthrottledClient; + } else { + return new ThrottlingUaaClient(unthrottledClient, environmentRequestLimit); + } } @Bean(initMethod = "block") @@ -249,7 +250,6 @@ String clientSecret(NameFactory nameFactory) { } @Bean - @DependsOn("uaaRatelimit") CloudFoundryCleaner cloudFoundryCleaner( @Qualifier("admin") CloudFoundryClient cloudFoundryClient, NameFactory nameFactory, @@ -326,54 +326,6 @@ DefaultConnectionContext connectionContext( return connectionContext.build(); } - @Bean - public Integer uaaLimiterMapping( - @Value("${uaa.api.request.limit:#{null}}") Integer environmentRequestLimit) { - return environmentRequestLimit; - } - - @Bean - Boolean uaaRatelimit( - ConnectionContext connectionContext, @Qualifier("admin") UaaClient uaaClient) { - return uaaClient - .rateLimit() - .getRatelimit(RatelimitRequest.builder().build()) - .map(response -> getServerRatelimit(response)) - .timeout(Duration.ofSeconds(5)) - .onErrorResume( - ex -> { - logger.error( - "Warning: could not fetch UAA rate limit, using default" - + " " - + 0 - + ". Cause: " - + ex); - return Mono.just(false); - }) - .block(); - } - - private Boolean getServerRatelimit(RatelimitResponse response) { - Current curr = response.getCurrentData(); - if (!"ACTIVE".equals(curr.getStatus())) { - logger.debug( - "UaaRatelimitInitializer server ratelimit is not 'ACTIVE', but " - + curr.getStatus() - + ". Ignoring server value for ratelimit."); - return false; - } - Integer result = curr.getLimiterMappings(); - logger.info( - "Server uses uaa rate limiting. There are " - + result - + " mappings declared in file " - + response.getFromSource()); - logger.info( - "If you encounter 429 return codes, configure uaa rate limiting or set variable" - + " 'UAA_API_REQUEST_LIMIT' to a save value."); - return true; - } - @Bean DopplerClient dopplerClient(ConnectionContext connectionContext, TokenProvider tokenProvider) { return ReactorDopplerClient.builder() @@ -701,13 +653,17 @@ PasswordGrantTokenProvider tokenProvider( UaaClient uaaClient( ConnectionContext connectionContext, TokenProvider tokenProvider, - int uaaLimiterMapping) { - return new ThrottlingUaaClient( + @Value("${uaa.api.request.limit:#{null}}") Integer environmentRequestLimit) { + ReactorUaaClient unthrottledClient = ReactorUaaClient.builder() .connectionContext(connectionContext) .tokenProvider(tokenProvider) - .build(), - uaaLimiterMapping); + .build(); + if (environmentRequestLimit == null) { + return unthrottledClient; + } else { + return new ThrottlingUaaClient(unthrottledClient, environmentRequestLimit); + } } @Bean(initMethod = "block") @@ -790,7 +746,7 @@ String username(NameFactory nameFactory) { return nameFactory.getUserName(); } - private static final class FailingDeserializationProblemHandler + public static final class FailingDeserializationProblemHandler extends DeserializationProblemHandler { @Override diff --git a/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java b/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java index 97923e2d73..a4a9d27a61 100644 --- a/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java +++ b/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java @@ -69,22 +69,29 @@ public class ThrottlingUaaClient implements UaaClient { private final UaaClient delegate; - + private final int maxRequestsPerSecond; private final RateLimiter rateLimiter; private final ThrottledUsers users; private Groups groups; - public ThrottlingUaaClient(ReactorUaaClient delegate, int uaaLimit) { + /** + * An {@link UaaClient} implementation that throttles calls to the UAA + * {@code /Groups} and {@code /Users} endpoints. It uses a single "bucket" + * for throttling requests to both endpoints. + * + * @see resilience4j docs + */ + public ThrottlingUaaClient(ReactorUaaClient delegate, int maxRequestsPerSecond) { // uaaLimit is calls per second. We need the milliseconds for one call because // resilience4j uses sliced timeslots, while the uaa server uses a sliding window. - int timeBasePerRequest = 1000 / uaaLimit; + int clockSkewMillis = 20; // 20ms clock skew is a save value for ~5 requests per second. + int rateLimitRefreshPeriodMillis = (1000 / maxRequestsPerSecond) + clockSkewMillis; this.delegate = delegate; + this.maxRequestsPerSecond = maxRequestsPerSecond; RateLimiterConfig config = RateLimiterConfig.custom() .limitForPeriod(1) - .limitRefreshPeriod( - Duration.ofMillis( - timeBasePerRequest + 20)) // 20 ms to handle clock skew. + .limitRefreshPeriod(Duration.ofMillis(rateLimitRefreshPeriodMillis)) .timeoutDuration(Duration.ofSeconds(10)) .build(); this.rateLimiter = RateLimiter.of("uaa", config); @@ -144,6 +151,10 @@ public Ratelimit rateLimit() { return this.delegate.rateLimit(); } + public int getMaxRequestsPerSecond() { + return maxRequestsPerSecond; + } + public class ThrottledUsers implements Users { private final Users usersDelegate; diff --git a/integration-test/src/test/java/org/cloudfoundry/uaa/RatelimitTestConfiguration.java b/integration-test/src/test/java/org/cloudfoundry/uaa/RatelimitTestConfiguration.java new file mode 100644 index 0000000000..e162955e4d --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/uaa/RatelimitTestConfiguration.java @@ -0,0 +1,73 @@ +package org.cloudfoundry.uaa; + +import java.time.Duration; +import org.cloudfoundry.IntegrationTestConfiguration.FailingDeserializationProblemHandler; +import org.cloudfoundry.ThrottlingUaaClient; +import org.cloudfoundry.reactor.ConnectionContext; +import org.cloudfoundry.reactor.DefaultConnectionContext; +import org.cloudfoundry.reactor.ProxyConfiguration; +import org.cloudfoundry.reactor.tokenprovider.ClientCredentialsGrantTokenProvider; +import org.cloudfoundry.reactor.uaa.ReactorUaaClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +@Configuration +@EnableAutoConfiguration +public class RatelimitTestConfiguration { + + @Bean + UaaClient adminUaaClient( + ConnectionContext connectionContext, + @Value("${test.admin.clientId}") String clientId, + @Value("${test.admin.clientSecret}") String clientSecret, + @Value("${uaa.api.request.limit:0}") int uaaLimit) { + ReactorUaaClient client = + ReactorUaaClient.builder() + .connectionContext(connectionContext) + .tokenProvider( + ClientCredentialsGrantTokenProvider.builder() + .clientId(clientId) + .clientSecret(clientSecret) + .build()) + .build(); + if (uaaLimit > 0) { + return new ThrottlingUaaClient(client, uaaLimit); + } + return client; + } + + @Bean + DefaultConnectionContext connectionContext( + @Value("${test.apiHost}") String apiHost, + @Value("${test.proxy.host:}") String proxyHost, + @Value("${test.proxy.password:}") String proxyPassword, + @Value("${test.proxy.port:8080}") Integer proxyPort, + @Value("${test.proxy.username:}") String proxyUsername, + @Value("${test.skipSslValidation:false}") Boolean skipSslValidation) { + + DefaultConnectionContext.Builder connectionContext = + DefaultConnectionContext.builder() + .apiHost(apiHost) + .problemHandler( + new FailingDeserializationProblemHandler()) // Test-only problem + // handler + .skipSslValidation(skipSslValidation) + .sslHandshakeTimeout(Duration.ofSeconds(30)); + + if (StringUtils.hasText(proxyHost)) { + ProxyConfiguration.Builder proxyConfiguration = + ProxyConfiguration.builder().host(proxyHost).port(proxyPort); + + if (StringUtils.hasText(proxyUsername)) { + proxyConfiguration.password(proxyPassword).username(proxyUsername); + } + + connectionContext.proxyConfiguration(proxyConfiguration.build()); + } + + return connectionContext.build(); + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/uaa/UaaRatelimitTest.java b/integration-test/src/test/java/org/cloudfoundry/uaa/UaaRatelimitTest.java new file mode 100644 index 0000000000..116b20338d --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/uaa/UaaRatelimitTest.java @@ -0,0 +1,72 @@ +package org.cloudfoundry.uaa; + +import java.time.Duration; +import org.cloudfoundry.ThrottlingUaaClient; +import org.cloudfoundry.uaa.ratelimit.Current; +import org.cloudfoundry.uaa.ratelimit.RatelimitRequest; +import org.cloudfoundry.uaa.ratelimit.RatelimitResponse; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +@SpringJUnitConfig(classes = RatelimitTestConfiguration.class) +public class UaaRatelimitTest { + private static final Logger LOGGER = LoggerFactory.getLogger(UaaRatelimitTest.class); + + @Autowired private UaaClient adminUaaClient; + + @Test + public void getRatelimit() { + int envRatelimit; + if (adminUaaClient instanceof ThrottlingUaaClient) { + ThrottlingUaaClient throttlingClient = (ThrottlingUaaClient) adminUaaClient; + envRatelimit = throttlingClient.getMaxRequestsPerSecond(); + } else { + envRatelimit = 0; + } + Mono tmp = + adminUaaClient + .rateLimit() + .getRatelimit(RatelimitRequest.builder().build()) + .map(response -> getServerRatelimit(response, envRatelimit)) + .timeout(Duration.ofSeconds(5)) + .onErrorResume( + ex -> { + LOGGER.error( + "Warning: could not fetch UAA rate limit, using default" + + " " + + 0 + + ". Cause: " + + ex); + return Mono.just(false); + }); + StepVerifier.create(tmp.materialize()).expectNextCount(1).verifyComplete(); + } + + private Boolean getServerRatelimit(RatelimitResponse response, int maxRequestsPerSecond) { + Current curr = response.getCurrentData(); + if (!"ACTIVE".equals(curr.getStatus())) { + LOGGER.debug( + "UaaRatelimitInitializer server ratelimit is not 'ACTIVE', but " + + curr.getStatus() + + ". Ignoring server value for ratelimit."); + return false; + } + Integer result = curr.getLimiterMappings(); + LOGGER.info( + "Server uses uaa rate limiting. There are " + + result + + " mappings declared in file " + + response.getFromSource()); + if (maxRequestsPerSecond == 0) { + LOGGER.warn( + "Ratelimiting is not set locally, set variable 'UAA_API_REQUEST_LIMIT' to a" + + " save value or you might experience http 429 responses."); + } + return true; + } +} diff --git a/integration-test/src/test/resources/logback-test.xml b/integration-test/src/test/resources/logback-test.xml index 0872bbe5aa..3c774460d3 100644 --- a/integration-test/src/test/resources/logback-test.xml +++ b/integration-test/src/test/resources/logback-test.xml @@ -26,7 +26,7 @@ - + @@ -36,6 +36,7 @@ + diff --git a/pom.xml b/pom.xml index 7fa977a981..df2a45ed6e 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ 3.0.2 2.44.4 + 1.7.0