diff --git a/src/main/java/com/auth0/exception/RateLimitException.java b/src/main/java/com/auth0/exception/RateLimitException.java index 738a941f1..9d2a15a77 100644 --- a/src/main/java/com/auth0/exception/RateLimitException.java +++ b/src/main/java/com/auth0/exception/RateLimitException.java @@ -1,5 +1,7 @@ package com.auth0.exception; +import com.auth0.net.TokenQuotaBucket; + import java.util.Map; /** @@ -16,6 +18,10 @@ public class RateLimitException extends APIException { private final long remaining; private final long reset; + private TokenQuotaBucket clientQuotaLimit; + private TokenQuotaBucket organizationQuotaLimit; + private long retryAfter; + private static final int STATUS_CODE_TOO_MANY_REQUEST = 429; public RateLimitException(long limit, long remaining, long reset, Map values) { @@ -56,4 +62,119 @@ public long getReset() { return reset; } + /** + * Getter for the client quota limit. + * @return The client quota limit or null if missing. + */ + public TokenQuotaBucket getClientQuotaLimit() { + return clientQuotaLimit; + } + + /** + * Getter for the organization quota limit. + * @return The organization quota limit or null if missing. + */ + public TokenQuotaBucket getOrganizationQuotaLimit() { + return organizationQuotaLimit; + } + + /** + * Getter for the retry after time in seconds. + * @return The retry after time in seconds or -1 if missing. + */ + public long getRetryAfter() { + return retryAfter; + } + + /** + * Builder class for creating instances of RateLimitException. + */ + public static class Builder { + private long limit; + private long remaining; + private long reset; + private TokenQuotaBucket clientQuotaLimit; + private TokenQuotaBucket organizationQuotaLimit; + private long retryAfter; + private Map values; + + /** + * Constructor for the Builder. + * @param limit The maximum number of requests available in the current time frame. + * @param remaining The number of remaining requests in the current time frame. + * @param reset The UNIX timestamp of the expected time when the rate limit will reset. + */ + public Builder(long limit, long remaining, long reset) { + this.limit = limit; + this.remaining = remaining; + this.reset = reset; + } + + /** + * Constructor for the Builder. + * @param limit The maximum number of requests available in the current time frame. + * @param remaining The number of remaining requests in the current time frame. + * @param reset The UNIX timestamp of the expected time when the rate limit will reset. + * @param values The values map. + */ + public Builder(long limit, long remaining, long reset, Map values) { + this.limit = limit; + this.remaining = remaining; + this.reset = reset; + this.values = values; + } + + /** + * Sets the client quota limit. + * @param clientQuotaLimit The client quota limit. + * @return The Builder instance. + */ + public Builder clientQuotaLimit(TokenQuotaBucket clientQuotaLimit) { + this.clientQuotaLimit = clientQuotaLimit; + return this; + } + + /** + * Sets the organization quota limit. + * @param organizationQuotaLimit The organization quota limit. + * @return The Builder instance. + */ + public Builder organizationQuotaLimit(TokenQuotaBucket organizationQuotaLimit) { + this.organizationQuotaLimit = organizationQuotaLimit; + return this; + } + + /** + * Sets the retry after time in seconds. + * @param retryAfter The retry after time in seconds. + * @return The Builder instance. + */ + public Builder retryAfter(long retryAfter) { + this.retryAfter = retryAfter; + return this; + } + + /** + * Sets the values map. + * @param values The values map. + * @return The Builder instance. + */ + public Builder values(Map values) { + this.values = values; + return this; + } + + public RateLimitException build() { + RateLimitException exception = (this.values != null) + ? new RateLimitException(this.limit, this.remaining, this.reset, this.values) + : new RateLimitException(this.limit, this.remaining, this.reset); + + exception.clientQuotaLimit = this.clientQuotaLimit; + exception.organizationQuotaLimit = this.organizationQuotaLimit; + exception.retryAfter = this.retryAfter; + + return exception; + } + } + } diff --git a/src/main/java/com/auth0/json/mgmt/client/Client.java b/src/main/java/com/auth0/json/mgmt/client/Client.java index 0b0e705e1..35be65d26 100644 --- a/src/main/java/com/auth0/json/mgmt/client/Client.java +++ b/src/main/java/com/auth0/json/mgmt/client/Client.java @@ -1,5 +1,6 @@ package com.auth0.json.mgmt.client; +import com.auth0.json.mgmt.tokenquota.TokenQuota; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -104,6 +105,8 @@ public class Client { private Boolean requireProofOfPossession; @JsonProperty("default_organization") private ClientDefaultOrganization defaultOrganization; + @JsonProperty("token_quota") + private TokenQuota tokenQuota; /** * Getter for the name of the tenant this client belongs to. @@ -907,5 +910,21 @@ public ClientDefaultOrganization getDefaultOrganization() { public void setDefaultOrganization(ClientDefaultOrganization defaultOrganization) { this.defaultOrganization = defaultOrganization; } + + /** + * Getter for the token quota configuration. + * @return the token quota configuration. + */ + public TokenQuota getTokenQuota() { + return tokenQuota; + } + + /** + * Setter for the token quota configuration. + * @param tokenQuota the token quota configuration to set. + */ + public void setTokenQuota(TokenQuota tokenQuota) { + this.tokenQuota = tokenQuota; + } } diff --git a/src/main/java/com/auth0/json/mgmt/organizations/Organization.java b/src/main/java/com/auth0/json/mgmt/organizations/Organization.java index 4d298f0c5..1b7142492 100644 --- a/src/main/java/com/auth0/json/mgmt/organizations/Organization.java +++ b/src/main/java/com/auth0/json/mgmt/organizations/Organization.java @@ -1,6 +1,7 @@ package com.auth0.json.mgmt.organizations; import com.auth0.client.mgmt.filter.PageFilter; +import com.auth0.json.mgmt.tokenquota.TokenQuota; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -29,6 +30,8 @@ public class Organization { private Branding branding; @JsonProperty("enabled_connections") private List enabledConnections; + @JsonProperty("token_quota") + private TokenQuota tokenQuota; public Organization() {} @@ -132,4 +135,20 @@ public List getEnabledConnections() { public void setEnabledConnections(List enabledConnections) { this.enabledConnections = enabledConnections; } + + /** + * @return the token quota of this Organization. + */ + public TokenQuota getTokenQuota() { + return tokenQuota; + } + + /** + * Sets the token quota of this Organization. + * + * @param tokenQuota the token quota of this Organization. + */ + public void setTokenQuota(TokenQuota tokenQuota) { + this.tokenQuota = tokenQuota; + } } diff --git a/src/main/java/com/auth0/json/mgmt/tenants/Clients.java b/src/main/java/com/auth0/json/mgmt/tenants/Clients.java new file mode 100644 index 000000000..49db8743e --- /dev/null +++ b/src/main/java/com/auth0/json/mgmt/tenants/Clients.java @@ -0,0 +1,35 @@ +package com.auth0.json.mgmt.tenants; + +import com.auth0.json.mgmt.tokenquota.ClientCredentials; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Clients { + @JsonProperty("client_credentials") + private ClientCredentials clientCredentials; + + /** + * Default constructor for Clients. + */ + public Clients() { + } + + /** + * Constructor for Clients. + * + * @param clientCredentials the client credentials + */ + public Clients(ClientCredentials clientCredentials) { + this.clientCredentials = clientCredentials; + } + + /** + * @return the client credentials + */ + public ClientCredentials getClientCredentials() { + return clientCredentials; + } +} diff --git a/src/main/java/com/auth0/json/mgmt/tenants/DefaultTokenQuota.java b/src/main/java/com/auth0/json/mgmt/tenants/DefaultTokenQuota.java new file mode 100644 index 000000000..2d5ae414d --- /dev/null +++ b/src/main/java/com/auth0/json/mgmt/tenants/DefaultTokenQuota.java @@ -0,0 +1,59 @@ +package com.auth0.json.mgmt.tenants; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DefaultTokenQuota { + @JsonProperty("clients") + private Clients clients; + + @JsonProperty("organizations") + private Organizations organizations; + + /** + * Default constructor for DefaultTokenQuota. + */ + public DefaultTokenQuota() {} + + /** + * Constructor for DefaultTokenQuota. + * + * @param clients the clients + * @param organizations the organizations + */ + public DefaultTokenQuota(Clients clients, Organizations organizations) { + this.clients = clients; + this.organizations = organizations; + } + + /** + * @return the clients + */ + public Clients getClients() { + return clients; + } + + /** + * @param clients the clients to set + */ + public void setClients(Clients clients) { + this.clients = clients; + } + + /** + * @return the organizations + */ + public Organizations getOrganizations() { + return organizations; + } + + /** + * @param organizations the organizations to set + */ + public void setOrganizations(Organizations organizations) { + this.organizations = organizations; + } +} diff --git a/src/main/java/com/auth0/json/mgmt/tenants/Organizations.java b/src/main/java/com/auth0/json/mgmt/tenants/Organizations.java new file mode 100644 index 000000000..56c2d85d8 --- /dev/null +++ b/src/main/java/com/auth0/json/mgmt/tenants/Organizations.java @@ -0,0 +1,34 @@ +package com.auth0.json.mgmt.tenants; + +import com.auth0.json.mgmt.tokenquota.ClientCredentials; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Organizations { + @JsonProperty("client_credentials") + private ClientCredentials clientCredentials; + + /** + * Default constructor for Organizations. + */ + public Organizations() {} + + /** + * Constructor for Organizations. + * + * @param clientCredentials the client credentials + */ + public Organizations(ClientCredentials clientCredentials) { + this.clientCredentials = clientCredentials; + } + + /** + * @return the client credentials + */ + public ClientCredentials getClientCredentials() { + return clientCredentials; + } +} diff --git a/src/main/java/com/auth0/json/mgmt/tenants/Tenant.java b/src/main/java/com/auth0/json/mgmt/tenants/Tenant.java index a48dc8c52..817db0007 100644 --- a/src/main/java/com/auth0/json/mgmt/tenants/Tenant.java +++ b/src/main/java/com/auth0/json/mgmt/tenants/Tenant.java @@ -58,6 +58,11 @@ public class Tenant { @JsonProperty("mtls") private Mtls mtls; + @JsonProperty("authorization_response_iss_parameter_supported") + private boolean authorizationResponseIssParameterSupported; + + @JsonProperty("default_token_quota") + private DefaultTokenQuota defaultTokenQuota; /** * Getter for the change password page customization. @@ -397,4 +402,36 @@ public Mtls getMtls() { public void setMtls(Mtls mtls) { this.mtls = mtls; } + + /** + * @return the value of the {@code authorization_response_iss_parameter_supported} field + */ + public boolean isAuthorizationResponseIssParameterSupported() { + return authorizationResponseIssParameterSupported; + } + + /** + * Sets the value of the {@code authorization_response_iss_parameter_supported} field + * + * @param authorizationResponseIssParameterSupported the value of the {@code authorization_response_iss_parameter_supported} field + */ + public void setAuthorizationResponseIssParameterSupported(boolean authorizationResponseIssParameterSupported) { + this.authorizationResponseIssParameterSupported = authorizationResponseIssParameterSupported; + } + + /** + * @return the value of the {@code default_token_quota} field + */ + public DefaultTokenQuota getDefaultTokenQuota() { + return defaultTokenQuota; + } + + /** + * Sets the value of the {@code default_token_quota} field + * + * @param defaultTokenQuota the value of the {@code default_token_quota} field + */ + public void setDefaultTokenQuota(DefaultTokenQuota defaultTokenQuota) { + this.defaultTokenQuota = defaultTokenQuota; + } } diff --git a/src/main/java/com/auth0/json/mgmt/tokenquota/ClientCredentials.java b/src/main/java/com/auth0/json/mgmt/tokenquota/ClientCredentials.java new file mode 100644 index 000000000..b7d52ef4c --- /dev/null +++ b/src/main/java/com/auth0/json/mgmt/tokenquota/ClientCredentials.java @@ -0,0 +1,81 @@ +package com.auth0.json.mgmt.tokenquota; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ClientCredentials { + @JsonProperty("per_day") + private Integer perDay; + @JsonProperty("per_hour") + private Integer perHour; + @JsonProperty("enforce") + private boolean enforce; + /** + * Default constructor for ClientCredentials. + */ + public ClientCredentials() {} + + /** + * Constructor for ClientCredentials. + * + * @param perDay the number of client credentials allowed per day + * @param perHour the number of client credentials allowed per hour + * @param enforce true if the quota is enforced, false otherwise + */ + public ClientCredentials(Integer perDay, Integer perHour, boolean enforce) { + this.perDay = perDay; + this.perHour = perHour; + this.enforce = enforce; + } + + /** + * @return the number of client credentials allowed per day + */ + public Integer getPerDay() { + return perDay; + } + + /** + * Sets the number of client credentials allowed per day. + * + * @param perDay the number of client credentials allowed per day + */ + public void setPerDay(Integer perDay) { + this.perDay = perDay; + } + + /** + * @return the number of client credentials allowed per hour + */ + public Integer getPerHour() { + return perHour; + } + + /** + * Sets the number of client credentials allowed per hour. + * + * @param perHour the number of client credentials allowed per hour + */ + public void setPerHour(Integer perHour) { + this.perHour = perHour; + } + + /** + * @return true if the quota is enforced, false otherwise + */ + public boolean isEnforce() { + return enforce; + } + + /** + * Sets whether the quota is enforced. + * + * @param enforce true to enforce the quota, false otherwise + */ + public void setEnforce(boolean enforce) { + this.enforce = enforce; + } +} diff --git a/src/main/java/com/auth0/json/mgmt/tokenquota/TokenQuota.java b/src/main/java/com/auth0/json/mgmt/tokenquota/TokenQuota.java new file mode 100644 index 000000000..e11dadac2 --- /dev/null +++ b/src/main/java/com/auth0/json/mgmt/tokenquota/TokenQuota.java @@ -0,0 +1,32 @@ +package com.auth0.json.mgmt.tokenquota; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TokenQuota { + @JsonProperty("client_credentials") + private ClientCredentials clientCredentials; + + /** + * Default constructor for Clients. + */ + public TokenQuota() {} + /** + * Constructor for Clients. + * + * @param clientCredentials the client credentials + */ + public TokenQuota(ClientCredentials clientCredentials) { + this.clientCredentials = clientCredentials; + } + + /** + * @return the client credentials + */ + public ClientCredentials getClientCredentials() { + return clientCredentials; + } +} diff --git a/src/main/java/com/auth0/net/BaseRequest.java b/src/main/java/com/auth0/net/BaseRequest.java index 44f5e1b4d..2ddcfc5f0 100644 --- a/src/main/java/com/auth0/net/BaseRequest.java +++ b/src/main/java/com/auth0/net/BaseRequest.java @@ -6,6 +6,7 @@ import com.auth0.exception.RateLimitException; import com.auth0.json.ObjectMapperProvider; import com.auth0.net.client.*; +import com.auth0.utils.HttpResponseHeadersUtils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.MapType; @@ -219,13 +220,29 @@ private RateLimitException createRateLimitException(Auth0HttpResponse response) long remaining = Long.parseLong(response.getHeader("x-ratelimit-remaining", "-1")); long reset = Long.parseLong(response.getHeader("x-ratelimit-reset", "-1")); + TokenQuotaBucket clientQuotaLimit = HttpResponseHeadersUtils.getClientQuotaLimit(response.getHeaders()); + TokenQuotaBucket organizationQuotaLimit = HttpResponseHeadersUtils.getOrganizationQuotaLimit(response.getHeaders()); + + long retryAfter = Long.parseLong(response.getHeader("retry-after", "-1")); + String payload = response.getBody(); MapType mapType = mapper.getTypeFactory().constructMapType(HashMap.class, String.class, Object.class); try { Map values = mapper.readValue(payload, mapType); - return new RateLimitException(limit, remaining, reset, values); + + RateLimitException.Builder builder = new RateLimitException.Builder(limit, remaining, reset, values); + + builder.clientQuotaLimit(clientQuotaLimit); + builder.organizationQuotaLimit(organizationQuotaLimit); + builder.retryAfter(retryAfter); + + return builder.build(); } catch (IOException e) { - return new RateLimitException(limit, remaining, reset); + RateLimitException.Builder builder = new RateLimitException.Builder(limit, remaining, reset); + builder.clientQuotaLimit(clientQuotaLimit); + builder.organizationQuotaLimit(organizationQuotaLimit); + builder.retryAfter(retryAfter); + return builder.build(); } } diff --git a/src/main/java/com/auth0/net/TokenQuotaBucket.java b/src/main/java/com/auth0/net/TokenQuotaBucket.java new file mode 100644 index 000000000..e6c192c21 --- /dev/null +++ b/src/main/java/com/auth0/net/TokenQuotaBucket.java @@ -0,0 +1,35 @@ +package com.auth0.net; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TokenQuotaBucket { + private TokenQuotaLimit perHour; + private TokenQuotaLimit perDay; + + /** + * Constructor for TokenQuotaBucket. + */ + public TokenQuotaBucket(TokenQuotaLimit perHour, TokenQuotaLimit perDay) { + this.perHour = perHour; + this.perDay = perDay; + } + + /** + * @return the number of client credentials allowed per hour + */ + public TokenQuotaLimit getPerHour() { + return perHour; + } + + /** + * @return the number of client credentials allowed per hour + */ + public TokenQuotaLimit getPerDay() { + return perDay; + } + + +} diff --git a/src/main/java/com/auth0/net/TokenQuotaLimit.java b/src/main/java/com/auth0/net/TokenQuotaLimit.java new file mode 100644 index 000000000..966147ed3 --- /dev/null +++ b/src/main/java/com/auth0/net/TokenQuotaLimit.java @@ -0,0 +1,30 @@ +package com.auth0.net; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TokenQuotaLimit { + private int quota; + private int remaining; + private int resetAfter; + + public TokenQuotaLimit(int quota, int remaining, int resetAfter) { + this.quota = quota; + this.remaining = remaining; + this.resetAfter = resetAfter; + } + + public int getQuota() { + return quota; + } + + public int getRemaining() { + return remaining; + } + + public int getResetAfter() { + return resetAfter; + } +} diff --git a/src/main/java/com/auth0/utils/HttpResponseHeadersUtils.java b/src/main/java/com/auth0/utils/HttpResponseHeadersUtils.java new file mode 100644 index 000000000..12d4b14dc --- /dev/null +++ b/src/main/java/com/auth0/utils/HttpResponseHeadersUtils.java @@ -0,0 +1,82 @@ +package com.auth0.utils; + +import com.auth0.net.TokenQuotaLimit; +import com.auth0.net.TokenQuotaBucket; + +import java.util.Map; + +public class HttpResponseHeadersUtils { + + /** + * Gets the client token quota limits from the provided headers. + * + * @param headers the HTTP response headers. + * @return a TokenQuotaBucket containing client rate limits, or null if not present. + */ + public static TokenQuotaBucket getClientQuotaLimit(Map headers) { + String quotaHeader = headers.get("auth0-client-quota-limit"); + if (quotaHeader != null) { + return parseQuota(quotaHeader); + } + return null; + } + + /** + * Gets the organization token quota limits from the provided headers. + * + * @param headers the HTTP response headers. + * @return a TokenQuotaBucket containing organization rate limits, or null if not present. + */ + public static TokenQuotaBucket getOrganizationQuotaLimit(Map headers) { + String quotaHeader = headers.get("auth0-organization-quota-limit"); + if (quotaHeader != null) { + return parseQuota(quotaHeader); + } + return null; + } + + public static TokenQuotaBucket parseQuota(String tokenQuota) { + + TokenQuotaLimit perHour = null; + TokenQuotaLimit perDay = null; + + String[] parts = tokenQuota.split(","); + for (String part : parts) { + String[] attributes = part.split(";"); + int quota = 0, remaining = 0, time = 0; + + for (String attribute : attributes) { + String[] keyValue = attribute.split("="); + if (keyValue.length != 2) continue; + + String key = keyValue[0].trim(); + String value = keyValue[1].trim(); + + switch (key) { + case "q": + quota = Integer.parseInt(value); + break; + case "r": + remaining = Integer.parseInt(value); + break; + case "t": + time = Integer.parseInt(value); + break; + } + } + + if (attributes.length >0 && attributes[0].contains("per_hour")) { + perHour = new TokenQuotaLimit(quota, remaining, time); + } else if (attributes.length >0 && attributes[0].contains("per_day")) { + perDay = new TokenQuotaLimit(quota, remaining, time); + } + } + + if(perHour == null && perDay == null) { + return null; + } + + return new TokenQuotaBucket(perHour, perDay); + } + +} diff --git a/src/test/java/com/auth0/client/MockServer.java b/src/test/java/com/auth0/client/MockServer.java index 87c63ba5d..54a6b440d 100644 --- a/src/test/java/com/auth0/client/MockServer.java +++ b/src/test/java/com/auth0/client/MockServer.java @@ -1,5 +1,6 @@ package com.auth0.client; +import com.auth0.net.TokenQuotaBucket; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.databind.type.MapType; @@ -231,6 +232,39 @@ public void rateLimitReachedResponse(long limit, long remaining, long reset, Str server.enqueue(response); } + public void rateLimitReachedResponse(long limit, long remaining, long reset, + String clientQuotaLimit, String organizationQuotaLimit, long retryAfter) throws IOException { + rateLimitReachedResponse(limit, remaining, reset, null, clientQuotaLimit, organizationQuotaLimit, retryAfter); + } + + public void rateLimitReachedResponse(long limit, long remaining, long reset, String path, String clientQuotaLimit, String organizationQuotaLimit, long retryAfter) throws IOException { + MockResponse response = new MockResponse().setResponseCode(429); + if (limit != -1) { + response.addHeader("x-ratelimit-limit", String.valueOf(limit)); + } + if (remaining != -1) { + response.addHeader("x-ratelimit-remaining", String.valueOf(remaining)); + } + if (reset != -1) { + response.addHeader("x-ratelimit-reset", String.valueOf(reset)); + } + if (clientQuotaLimit != null) { + response.addHeader("auth0-client-quota-limit", clientQuotaLimit); + } + if (organizationQuotaLimit != null) { + response.addHeader("auth0-organization-quota-limit", organizationQuotaLimit); + } + if(retryAfter != -1) { + response.addHeader("retry-after", String.valueOf(retryAfter)); + } + if (path != null) { + response + .addHeader("Content-Type", "application/json") + .setBody(readTextFile(path)); + } + server.enqueue(response); + } + public void textResponse(String path, int statusCode) throws IOException { MockResponse response = new MockResponse() .setResponseCode(statusCode) diff --git a/src/test/java/com/auth0/client/mgmt/ClientsEntityTest.java b/src/test/java/com/auth0/client/mgmt/ClientsEntityTest.java index 4e574254a..8c656d9ce 100644 --- a/src/test/java/com/auth0/client/mgmt/ClientsEntityTest.java +++ b/src/test/java/com/auth0/client/mgmt/ClientsEntityTest.java @@ -5,6 +5,8 @@ import com.auth0.json.mgmt.client.Client; import com.auth0.json.mgmt.client.ClientsPage; import com.auth0.json.mgmt.client.Credential; +import com.auth0.json.mgmt.tokenquota.ClientCredentials; +import com.auth0.json.mgmt.tokenquota.TokenQuota; import com.auth0.net.Request; import com.auth0.net.client.HttpMethod; import okhttp3.mockwebserver.RecordedRequest; @@ -163,7 +165,6 @@ public void shouldGetClient() throws Exception { assertThat(recordedRequest, hasMethodAndPath(HttpMethod.GET, "/api/v2/clients/1")); assertThat(recordedRequest, hasHeader("Content-Type", "application/json")); assertThat(recordedRequest, hasHeader("Authorization", "Bearer apiToken")); - assertThat(response, is(notNullValue())); } @@ -213,7 +214,15 @@ public void shouldThrowOnCreateClientWithNullData() { @Test public void shouldCreateClient() throws Exception { - Request request = api.clients().create(new Client("My Application")); + Client clientCreate = new Client("My Application"); + ClientCredentials clientCredentials = new ClientCredentials(); + clientCredentials.setPerDay(100); + clientCredentials.setPerHour(20); + clientCredentials.setEnforce(true); + + TokenQuota tokenQuota = new TokenQuota(clientCredentials); + clientCreate.setTokenQuota(tokenQuota); + Request request = api.clients().create(clientCreate); assertThat(request, is(notNullValue())); server.jsonResponse(MGMT_CLIENT, 200); @@ -225,9 +234,18 @@ public void shouldCreateClient() throws Exception { assertThat(recordedRequest, hasHeader("Authorization", "Bearer apiToken")); Map body = bodyFromRequest(recordedRequest); - assertThat(body.size(), is(1)); + assertThat(body.size(), is(2)); assertThat(body, hasEntry("name", "My Application")); + // Verify the token_quota structure + Map tokenQuotaMap = (Map) body.get("token_quota"); + assertThat(tokenQuotaMap, is(notNullValue())); + Map clientCredentialsMap = (Map) tokenQuotaMap.get("client_credentials"); + assertThat(clientCredentialsMap, is(notNullValue())); + assertThat(clientCredentialsMap, hasEntry("per_day", 100)); + assertThat(clientCredentialsMap, hasEntry("per_hour", 20)); + assertThat(clientCredentialsMap, hasEntry("enforce", true)); + assertThat(response, is(notNullValue())); } @@ -268,7 +286,15 @@ public void shouldThrowOnUpdateClientWithNullData() { @Test public void shouldUpdateClient() throws Exception { - Request request = api.clients().update("1", new Client("My Application")); + Client clientUpdate = new Client( "My Application"); + ClientCredentials clientCredentials = new ClientCredentials(); + clientCredentials.setPerDay(100); + clientCredentials.setPerHour(20); + clientCredentials.setEnforce(true); + + TokenQuota tokenQuota = new TokenQuota(clientCredentials); + clientUpdate.setTokenQuota(tokenQuota); + Request request = api.clients().update("1", clientUpdate); assertThat(request, is(notNullValue())); server.jsonResponse(MGMT_CLIENT, 200); @@ -280,9 +306,18 @@ public void shouldUpdateClient() throws Exception { assertThat(recordedRequest, hasHeader("Authorization", "Bearer apiToken")); Map body = bodyFromRequest(recordedRequest); - assertThat(body.size(), is(1)); + assertThat(body.size(), is(2)); assertThat(body, hasEntry("name", "My Application")); + Map tokenQuotaMap = (Map) body.get("token_quota"); + assertThat(tokenQuotaMap, is(notNullValue())); + Map clientCredentialsMap = (Map) tokenQuotaMap.get("client_credentials"); + assertThat(clientCredentialsMap, is(notNullValue())); + assertThat(clientCredentialsMap, hasEntry("per_day", 100)); + assertThat(clientCredentialsMap, hasEntry("per_hour", 20)); + assertThat(clientCredentialsMap, hasEntry("enforce", true)); + + assertThat(response, is(notNullValue())); } diff --git a/src/test/java/com/auth0/client/mgmt/OrganizationEntityTest.java b/src/test/java/com/auth0/client/mgmt/OrganizationEntityTest.java index bfa209562..2928ed95b 100644 --- a/src/test/java/com/auth0/client/mgmt/OrganizationEntityTest.java +++ b/src/test/java/com/auth0/client/mgmt/OrganizationEntityTest.java @@ -11,6 +11,8 @@ import com.auth0.json.mgmt.organizations.*; import com.auth0.json.mgmt.resourceserver.ResourceServer; import com.auth0.json.mgmt.roles.RolesPage; +import com.auth0.json.mgmt.tokenquota.ClientCredentials; +import com.auth0.json.mgmt.tokenquota.TokenQuota; import com.auth0.net.Request; import com.auth0.net.client.HttpMethod; import okhttp3.mockwebserver.RecordedRequest; @@ -181,6 +183,14 @@ public void shouldCreateOrganization() throws Exception { enabledConnections.add(enabledConnection); orgToCreate.setEnabledConnections(enabledConnections); + ClientCredentials clientCredentials = new ClientCredentials(); + clientCredentials.setPerDay(100); + clientCredentials.setPerHour(20); + clientCredentials.setEnforce(true); + + TokenQuota tokenQuota = new TokenQuota(clientCredentials); + orgToCreate.setTokenQuota(tokenQuota); + Request request = api.organizations().create(orgToCreate); assertThat(request, is(notNullValue())); @@ -193,12 +203,21 @@ public void shouldCreateOrganization() throws Exception { assertThat(recordedRequest, hasHeader("Authorization", "Bearer apiToken")); Map body = bodyFromRequest(recordedRequest); - assertThat(body, aMapWithSize(5)); + assertThat(body, aMapWithSize(6)); assertThat(body, hasEntry("name", "test-org")); assertThat(body, hasEntry("display_name", "display name")); assertThat(body, hasEntry("metadata", metadata)); assertThat(body, hasEntry(is("enabled_connections"), is(notNullValue()))); assertThat(body, hasEntry(is("branding"), is(notNullValue()))); + assertThat(body, hasEntry(is("token_quota"), is(notNullValue()))); + Map tokenQuotaMap = (Map) body.get("token_quota"); + assertThat(tokenQuotaMap, is(notNullValue())); + Map clientCredentialsMap = (Map) tokenQuotaMap.get("client_credentials"); + assertThat(clientCredentialsMap, is(notNullValue())); + assertThat(clientCredentialsMap, hasEntry("per_day", 100)); + assertThat(clientCredentialsMap, hasEntry("per_hour", 20)); + assertThat(clientCredentialsMap, hasEntry("enforce", true)); + assertThat(response, is(notNullValue())); } @@ -234,6 +253,14 @@ public void shouldUpdateOrganization() throws Exception { metadata.put("key1", "val1"); orgToUpdate.setMetadata(metadata); + ClientCredentials clientCredentials = new ClientCredentials(); + clientCredentials.setPerDay(100); + clientCredentials.setPerHour(20); + clientCredentials.setEnforce(true); + + TokenQuota tokenQuota = new TokenQuota(clientCredentials); + orgToUpdate.setTokenQuota(tokenQuota); + Request request = api.organizations().update("org_abc", orgToUpdate); assertThat(request, is(notNullValue())); @@ -246,11 +273,21 @@ public void shouldUpdateOrganization() throws Exception { assertThat(recordedRequest, hasHeader("Authorization", "Bearer apiToken")); Map body = bodyFromRequest(recordedRequest); - assertThat(body, aMapWithSize(4)); + assertThat(body, aMapWithSize(5)); assertThat(body, hasEntry("name", "test-org")); assertThat(body, hasEntry("display_name", "display name")); assertThat(body, hasEntry("metadata", metadata)); assertThat(body, hasEntry(is("branding"), is(notNullValue()))); + assertThat(body, hasEntry(is("token_quota"), is(notNullValue()))); + + Map tokenQuotaMap = (Map) body.get("token_quota"); + assertThat(tokenQuotaMap, is(notNullValue())); + Map clientCredentialsMap = (Map) tokenQuotaMap.get("client_credentials"); + assertThat(clientCredentialsMap, is(notNullValue())); + assertThat(clientCredentialsMap, hasEntry("per_day", 100)); + assertThat(clientCredentialsMap, hasEntry("per_hour", 20)); + assertThat(clientCredentialsMap, hasEntry("enforce", true)); + assertThat(response, is(notNullValue())); } diff --git a/src/test/java/com/auth0/client/mgmt/TenantsEntityTest.java b/src/test/java/com/auth0/client/mgmt/TenantsEntityTest.java index daa082f4e..5abe4e9c7 100644 --- a/src/test/java/com/auth0/client/mgmt/TenantsEntityTest.java +++ b/src/test/java/com/auth0/client/mgmt/TenantsEntityTest.java @@ -1,18 +1,25 @@ package com.auth0.client.mgmt; import com.auth0.client.mgmt.filter.FieldsFilter; +import com.auth0.json.mgmt.tenants.Clients; +import com.auth0.json.mgmt.tenants.DefaultTokenQuota; +import com.auth0.json.mgmt.tenants.Organizations; import com.auth0.json.mgmt.tenants.Tenant; +import com.auth0.json.mgmt.tokenquota.ClientCredentials; +import com.auth0.json.mgmt.tokenquota.TokenQuota; import com.auth0.net.Request; import com.auth0.net.client.HttpMethod; import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.Test; +import java.util.Map; + import static com.auth0.AssertsUtil.verifyThrows; import static com.auth0.client.MockServer.MGMT_TENANT; +import static com.auth0.client.MockServer.*; import static com.auth0.client.RecordedRequestMatcher.*; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.*; public class TenantsEntityTest extends BaseMgmtEntityTest { @@ -60,7 +67,14 @@ public void shouldThrowOnUpdateTenantSettingsWithNullData() { @Test public void shouldUpdateTenantSettings() throws Exception { - Request request = api.tenants().update(new Tenant()); + Tenant tenant = new Tenant(); + + DefaultTokenQuota defaultTokenQuota = new DefaultTokenQuota( + new Clients(new ClientCredentials(100, 20, true)), + new Organizations(new ClientCredentials(100, 20, true))); + + tenant.setDefaultTokenQuota(defaultTokenQuota); + Request request = api.tenants().update(tenant); assertThat(request, is(notNullValue())); server.jsonResponse(MGMT_TENANT, 200); @@ -71,6 +85,32 @@ public void shouldUpdateTenantSettings() throws Exception { assertThat(recordedRequest, hasHeader("Content-Type", "application/json")); assertThat(recordedRequest, hasHeader("Authorization", "Bearer apiToken")); + + // Parse and validate the request body + Map body = bodyFromRequest(recordedRequest); + assertThat(body, is(notNullValue())); + + Map tokenQuotaMap = (Map) body.get("default_token_quota"); + assertThat(tokenQuotaMap, is(notNullValue())); + + // Validate "clients" nested structure + Map clientsMap = (Map) tokenQuotaMap.get("clients"); + assertThat(clientsMap, is(notNullValue())); + Map clientCredentialsMap = (Map) clientsMap.get("client_credentials"); + assertThat(clientCredentialsMap, is(notNullValue())); + assertThat(clientCredentialsMap, hasEntry("per_day", 100)); + assertThat(clientCredentialsMap, hasEntry("per_hour", 20)); + assertThat(clientCredentialsMap, hasEntry("enforce", true)); + + // Validate "organizations" nested structure + Map organizationsMap = (Map) tokenQuotaMap.get("organizations"); + assertThat(organizationsMap, is(notNullValue())); + Map organizationCredentialsMap = (Map) organizationsMap.get("client_credentials"); + assertThat(organizationCredentialsMap, is(notNullValue())); + assertThat(organizationCredentialsMap, hasEntry("per_day", 100)); + assertThat(organizationCredentialsMap, hasEntry("per_hour", 20)); + assertThat(organizationCredentialsMap, hasEntry("enforce", true)); + assertThat(response, is(notNullValue())); } } diff --git a/src/test/java/com/auth0/json/mgmt/client/ClientTest.java b/src/test/java/com/auth0/json/mgmt/client/ClientTest.java index 05bbd465f..301976258 100644 --- a/src/test/java/com/auth0/json/mgmt/client/ClientTest.java +++ b/src/test/java/com/auth0/json/mgmt/client/ClientTest.java @@ -2,6 +2,8 @@ import com.auth0.json.JsonMatcher; import com.auth0.json.JsonTest; +import com.auth0.json.mgmt.tokenquota.ClientCredentials; +import com.auth0.json.mgmt.tokenquota.TokenQuota; import org.hamcrest.collection.IsMapContaining; import org.junit.jupiter.api.Test; @@ -140,7 +142,14 @@ public class ClientTest extends JsonTest { " \"default_organization\": {\n" + " \"flows\": [\"client_credentials\"],\n" + " \"organizations_id\": \"org_id\"\n" + - " }\n" + + " },\n" + + " \"token_quota\": {\n" + + " \"client_credentials\": {\n" + + " \"per_hour\": 10,\n" + + " \"per_day\": 100,\n" + + " \"enforce\": true\n" + + " }\n" + + " }" + "}"; @Test @@ -223,6 +232,14 @@ public void shouldSerialize() throws Exception { defaultOrganization.setOrganizationId("org_id"); client.setDefaultOrganization(defaultOrganization); + ClientCredentials clientCredentials = new ClientCredentials(); + clientCredentials.setPerDay(100); + clientCredentials.setPerHour(20); + clientCredentials.setEnforce(true); + + TokenQuota tokenQuota = new TokenQuota(clientCredentials); + client.setTokenQuota(tokenQuota); + String serialized = toJSON(client); assertThat(serialized, is(notNullValue())); @@ -264,6 +281,8 @@ public void shouldSerialize() throws Exception { assertThat(serialized, JsonMatcher.hasEntry("compliance_level", "fapi1_adv_pkj_par")); assertThat(serialized, JsonMatcher.hasEntry("require_proof_of_possession", true)); assertThat(serialized, JsonMatcher.hasEntry("default_organization", notNullValue())); + assertThat(serialized, JsonMatcher.hasEntry("token_quota", notNullValue())); + assertThat(serialized, containsString("\"token_quota\":{\"client_credentials\":{\"per_day\":100,\"per_hour\":20,\"enforce\":true}}")); } @Test @@ -341,6 +360,12 @@ public void shouldDeserialize() throws Exception { assertThat(client.getSignedRequest().getCredentials().get(0).getUpdatedAt(), is(Date.from(Instant.parse("2024-03-14T11:34:28.893Z")))); assertThat(client.getRequireProofOfPossession(), is(true)); + + assertThat(client.getTokenQuota(), is(notNullValue())); + assertThat(client.getTokenQuota().getClientCredentials(), is(notNullValue())); + assertThat(client.getTokenQuota().getClientCredentials().getPerDay(), is(100)); + assertThat(client.getTokenQuota().getClientCredentials().getPerHour(), is(10)); + assertThat(client.getTokenQuota().getClientCredentials().isEnforce(), is(true)); } @Test diff --git a/src/test/java/com/auth0/json/mgmt/organizations/OrganizationsTest.java b/src/test/java/com/auth0/json/mgmt/organizations/OrganizationsTest.java index 66aff9737..ea5cdd22f 100644 --- a/src/test/java/com/auth0/json/mgmt/organizations/OrganizationsTest.java +++ b/src/test/java/com/auth0/json/mgmt/organizations/OrganizationsTest.java @@ -2,6 +2,8 @@ import com.auth0.json.JsonMatcher; import com.auth0.json.JsonTest; +import com.auth0.json.mgmt.tokenquota.ClientCredentials; +import com.auth0.json.mgmt.tokenquota.TokenQuota; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -18,20 +20,27 @@ public class OrganizationsTest extends JsonTest { @Test public void shouldDeserialize() throws Exception { String orgJson = "{\n" + - " \"id\": \"org_abc\",\n" + - " \"name\": \"org-name\",\n" + - " \"display_name\": \"display name\",\n" + - " \"branding\": {\n" + - " \"logo_url\": \"https://some-url.com/\",\n" + - " \"colors\": {\n" + - " \"primary\": \"#FF0000\",\n" + - " \"page_background\": \"#FF0000\"\n" + - " }\n" + - " },\n" + - " \"metadata\": {\n" + - " \"key1\": \"val1\"\n" + + " \"id\": \"org_abc\",\n" + + " \"name\": \"org-name\",\n" + + " \"display_name\": \"display name\",\n" + + " \"branding\": {\n" + + " \"logo_url\": \"https://some-url.com/\",\n" + + " \"colors\": {\n" + + " \"primary\": \"#FF0000\",\n" + + " \"page_background\": \"#FF0000\"\n" + " }\n" + - " }"; + " },\n" + + " \"metadata\": {\n" + + " \"key1\": \"val1\"\n" + + " },\n" + + " \"token_quota\": {\n" + + " \"client_credentials\": {\n" + + " \"per_hour\": 10,\n" + + " \"per_day\": 100,\n" + + " \"enforce\": true\n" + + " }\n" + + " }\n" + + "}"; Organization org = fromJSON(orgJson, Organization.class); assertThat(org, is(notNullValue())); @@ -45,6 +54,11 @@ public void shouldDeserialize() throws Exception { assertThat(org.getBranding().getColors().getPageBackground(), is("#FF0000")); assertThat(org.getMetadata(), is(notNullValue())); assertThat(org.getMetadata().get("key1"), is("val1")); + assertThat(org.getTokenQuota(), is(notNullValue())); + assertThat(org.getTokenQuota().getClientCredentials(), is(notNullValue())); + assertThat(org.getTokenQuota().getClientCredentials().getPerHour(), is(10)); + assertThat(org.getTokenQuota().getClientCredentials().getPerDay(), is(100)); + assertThat(org.getTokenQuota().getClientCredentials().isEnforce(), is(true)); } @Test @@ -70,6 +84,9 @@ public void shouldSerialize() throws Exception { enabledConnections.add(enabledConnection); organization.setEnabledConnections(enabledConnections); + TokenQuota tokenQuota = new TokenQuota(new ClientCredentials(100, 10, true)); + organization.setTokenQuota(tokenQuota); + String serialized = toJSON(organization); assertThat(serialized, is(notNullValue())); assertThat(serialized, JsonMatcher.hasEntry("name", "org-name")); @@ -77,5 +94,14 @@ public void shouldSerialize() throws Exception { assertThat(serialized, JsonMatcher.hasEntry("metadata", metadata)); assertThat(serialized, JsonMatcher.hasEntry("enabled_connections", is(notNullValue()))); assertThat(serialized, JsonMatcher.hasEntry("branding", is(notNullValue()))); + assertThat(serialized, JsonMatcher.hasEntry("branding", JsonMatcher.hasEntry("logo_url", "https://some-url.com"))); + assertThat(serialized, JsonMatcher.hasEntry("branding", JsonMatcher.hasEntry("colors", is(notNullValue())))); + assertThat(serialized, JsonMatcher.hasEntry("branding", JsonMatcher.hasEntry("colors", JsonMatcher.hasEntry("primary", "#FF0000")))); + assertThat(serialized, JsonMatcher.hasEntry("branding", JsonMatcher.hasEntry("colors", JsonMatcher.hasEntry("page_background", "#DD0000")))); + assertThat(serialized, JsonMatcher.hasEntry("token_quota", is(notNullValue()))); + assertThat(serialized, JsonMatcher.hasEntry("token_quota", JsonMatcher.hasEntry("client_credentials", is(notNullValue())))); + assertThat(serialized, JsonMatcher.hasEntry("token_quota", JsonMatcher.hasEntry("client_credentials", JsonMatcher.hasEntry("per_hour", 10)))); + assertThat(serialized, JsonMatcher.hasEntry("token_quota", JsonMatcher.hasEntry("client_credentials", JsonMatcher.hasEntry("per_day", 100)))); + assertThat(serialized, JsonMatcher.hasEntry("token_quota", JsonMatcher.hasEntry("client_credentials", JsonMatcher.hasEntry("enforce", true)))); } } diff --git a/src/test/java/com/auth0/json/mgmt/tenants/TenantTest.java b/src/test/java/com/auth0/json/mgmt/tenants/TenantTest.java index b923d584c..de5f478e4 100644 --- a/src/test/java/com/auth0/json/mgmt/tenants/TenantTest.java +++ b/src/test/java/com/auth0/json/mgmt/tenants/TenantTest.java @@ -2,6 +2,7 @@ import com.auth0.json.JsonMatcher; import com.auth0.json.JsonTest; +import com.auth0.json.mgmt.tokenquota.ClientCredentials; import org.junit.jupiter.api.Test; import java.util.Arrays; @@ -12,7 +13,51 @@ public class TenantTest extends JsonTest { - private static final String json = "{\"change_password\":{},\"guardian_mfa_page\":{},\"default_audience\":\"https://domain.auth0.com/myapi\",\"default_directory\":\"Username-Password-Authentication\",\"error_page\":{},\"flags\":{},\"friendly_name\":\"My-Tenant\",\"picture_url\":\"https://pic.to/123\",\"support_email\":\"support@auth0.com\",\"support_url\":\"https://support.auth0.com\",\"allowed_logout_urls\":[\"https://domain.auth0.com/logout\"], \"session_lifetime\":24, \"idle_session_lifetime\":0.5, \"session_cookie\":{\"mode\": \"persistent\"}, \"acr_values_supported\":[\"string1\",\"string2\"], \"pushed_authorization_requests_supported\": true, \"remove_alg_from_jwks\": true, \"mtls\": {\"enable_endpoint_aliases\": true}}"; + private static final String json = "{\n" + + " \"change_password\": {},\n" + + " \"guardian_mfa_page\": {},\n" + + " \"default_audience\": \"https://domain.auth0.com/myapi\",\n" + + " \"default_directory\": \"Username-Password-Authentication\",\n" + + " \"error_page\": {},\n" + + " \"flags\": {},\n" + + " \"friendly_name\": \"My-Tenant\",\n" + + " \"picture_url\": \"https://pic.to/123\",\n" + + " \"support_email\": \"support@auth0.com\",\n" + + " \"support_url\": \"https://support.auth0.com\",\n" + + " \"allowed_logout_urls\": [\n" + + " \"https://domain.auth0.com/logout\"\n" + + " ],\n" + + " \"session_lifetime\": 24,\n" + + " \"idle_session_lifetime\": 0.5,\n" + + " \"session_cookie\": {\n" + + " \"mode\": \"persistent\"\n" + + " },\n" + + " \"acr_values_supported\": [\n" + + " \"string1\",\n" + + " \"string2\"\n" + + " ],\n" + + " \"pushed_authorization_requests_supported\": true,\n" + + " \"remove_alg_from_jwks\": true,\n" + + " \"mtls\": {\n" + + " \"enable_endpoint_aliases\": true\n" + + " },\n" + + " \"default_token_quota\": {\n" + + " \"clients\": {\n" + + " \"client_credentials\": {\n" + + " \"per_day\": 100,\n" + + " \"per_hour\": 20,\n" + + " \"enforce\": true\n" + + " }\n" + + " },\n" + + " \"organizations\": {\n" + + " \"client_credentials\": {\n" + + " \"per_day\": 100,\n" + + " \"per_hour\": 20,\n" + + " \"enforce\": true\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; @Test @@ -35,6 +80,15 @@ public void shouldSerialize() throws Exception { tenant.setAcrValuesSupported(Collections.singletonList("supported acr value")); tenant.setPushedAuthorizationRequestsSupported(true); tenant.setRemoveAlgFromJwks(true); + + ClientCredentials clientCredentials = new ClientCredentials(); + clientCredentials.setPerDay(100); + clientCredentials.setPerHour(20); + clientCredentials.setEnforce(true); + Clients clientTokenQuota = new Clients(clientCredentials); + Organizations organizationTokenQuota = new Organizations(clientCredentials); + tenant.setDefaultTokenQuota(new DefaultTokenQuota(clientTokenQuota, organizationTokenQuota)); + Mtls mtls = new Mtls(); mtls.setEnableEndpointAliases(true); tenant.setMtls(mtls); @@ -60,6 +114,8 @@ public void shouldSerialize() throws Exception { assertThat(serialized, JsonMatcher.hasEntry("pushed_authorization_requests_supported", true)); assertThat(serialized, JsonMatcher.hasEntry("remove_alg_from_jwks", true)); assertThat(serialized, JsonMatcher.hasEntry("enable_endpoint_aliases", notNullValue())); + assertThat(serialized, JsonMatcher.hasEntry("mtls", notNullValue())); + assertThat(serialized, JsonMatcher.hasEntry("default_token_quota", notNullValue())); } @Test @@ -87,6 +143,17 @@ public void shouldDeserialize() throws Exception { assertThat(tenant.getRemoveAlgFromJwks(), is(true)); assertThat(tenant.getMtls(), is(notNullValue())); assertThat(tenant.getMtls().getEnableEndpointAliases(), is(true)); + assertThat(tenant.getDefaultTokenQuota(), is(notNullValue())); + assertThat(tenant.getDefaultTokenQuota().getClients(), is(notNullValue())); + assertThat(tenant.getDefaultTokenQuota().getClients().getClientCredentials(), is(notNullValue())); + assertThat(tenant.getDefaultTokenQuota().getClients().getClientCredentials().getPerDay(), is(100)); + assertThat(tenant.getDefaultTokenQuota().getClients().getClientCredentials().getPerHour(), is(20)); + assertThat(tenant.getDefaultTokenQuota().getClients().getClientCredentials().isEnforce(), is(true)); + assertThat(tenant.getDefaultTokenQuota().getOrganizations(), is(notNullValue())); + assertThat(tenant.getDefaultTokenQuota().getOrganizations().getClientCredentials(), is(notNullValue())); + assertThat(tenant.getDefaultTokenQuota().getOrganizations().getClientCredentials().getPerDay(), is(100)); + assertThat(tenant.getDefaultTokenQuota().getOrganizations().getClientCredentials().getPerHour(), is(20)); + assertThat(tenant.getDefaultTokenQuota().getOrganizations().getClientCredentials().isEnforce(), is(true)); } } diff --git a/src/test/java/com/auth0/net/BaseRequestTest.java b/src/test/java/com/auth0/net/BaseRequestTest.java index 5fe9f0023..4521e8bd9 100644 --- a/src/test/java/com/auth0/net/BaseRequestTest.java +++ b/src/test/java/com/auth0/net/BaseRequestTest.java @@ -197,6 +197,8 @@ public void shouldAddHeaders() throws Exception { request.addParameter("non_empty", "body"); request.addHeader("Extra-Info", "this is a test"); request.addHeader("Authorization", "Bearer my_access_token"); + request.addHeader("X-Client-Quota", getTokenQuotaString()); + request.addHeader("X-Organization-Quota", getTokenQuotaString()); server.jsonResponse(AUTH_TOKENS, 200); request.execute().getBody(); @@ -425,6 +427,33 @@ public void shouldParseRateLimitException() throws Exception { assertThat(rateLimitException.getReset(), Matchers.is(5L)); } + @Test + public void shouldParseRateLimitExceptionWithZeroRemaining() throws Exception { + BaseRequest request = new BaseRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.GET, listType); + server.rateLimitReachedResponse(100, 0, 5, RATE_LIMIT_ERROR, getTokenQuotaString(), getTokenQuotaString(), 1000); + Exception exception = null; + try { + request.execute().getBody(); + server.takeRequest(); + } catch (Exception e) { + exception = e; + } + assertThat(exception, Matchers.is(notNullValue())); + assertThat(exception, Matchers.is(Matchers.instanceOf(RateLimitException.class))); + assertThat(exception.getCause(), Matchers.is(nullValue())); + assertThat(exception.getMessage(), Matchers.is("Request failed with status code 429: Global limit has been reached")); + RateLimitException rateLimitException = (RateLimitException) exception; + assertThat(rateLimitException.getDescription(), Matchers.is("Global limit has been reached")); + assertThat(rateLimitException.getError(), Matchers.is("too_many_requests")); + assertThat(rateLimitException.getValue("non_existing_key"), Matchers.is(nullValue())); + assertThat(rateLimitException.getStatusCode(), Matchers.is(429)); + assertThat(rateLimitException.getLimit(), Matchers.is(100L)); + assertThat(rateLimitException.getRemaining(), Matchers.is(0L)); + assertThat(rateLimitException.getReset(), Matchers.is(5L)); + assertThat(rateLimitException.getClientQuotaLimit().getPerDay().getQuota(), Matchers.is(getTokenQuota().getPerDay().getQuota())); + assertThat(rateLimitException.getOrganizationQuotaLimit().getPerDay().getQuota(), Matchers.is(getTokenQuota().getPerDay().getQuota())); + } + @Test public void shouldDefaultRateLimitsHeadersWhenMissing() throws Exception { BaseRequest request = new BaseRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.GET, listType); @@ -448,5 +477,31 @@ public void shouldDefaultRateLimitsHeadersWhenMissing() throws Exception { assertThat(rateLimitException.getLimit(), Matchers.is(-1L)); assertThat(rateLimitException.getRemaining(), Matchers.is(-1L)); assertThat(rateLimitException.getReset(), Matchers.is(-1L)); + assertThat(rateLimitException.getClientQuotaLimit(), Matchers.is(nullValue())); + assertThat(rateLimitException.getOrganizationQuotaLimit(), Matchers.is(nullValue())); + } + + private TokenQuotaBucket getTokenQuota() { + TokenQuotaLimit perHourLimit = new TokenQuotaLimit(100, 80, 3600); + TokenQuotaLimit perDayLimit = new TokenQuotaLimit(100, 90, 86400); + return new TokenQuotaBucket(perHourLimit, perDayLimit); + } + + public String getTokenQuotaString() { + TokenQuotaLimit perHourLimit = new TokenQuotaLimit(100, 80, 3600); + TokenQuotaLimit perDayLimit = new TokenQuotaLimit(100, 90, 86400); + + StringBuilder builder = new StringBuilder(); + + builder.append(String.format("b=per_hour;q=%d;r=%d;t=%d", + perHourLimit.getQuota(), perHourLimit.getRemaining(), perHourLimit.getResetAfter())); + + if (builder.length() > 0) { + builder.append(","); + } + builder.append(String.format("b=per_day;q=%d;r=%d;t=%d", + perDayLimit.getQuota(), perDayLimit.getRemaining(), perDayLimit.getResetAfter())); + + return builder.toString(); } } diff --git a/src/test/java/com/auth0/net/MultipartRequestTest.java b/src/test/java/com/auth0/net/MultipartRequestTest.java index 9eb1f61f7..848a8df8b 100644 --- a/src/test/java/com/auth0/net/MultipartRequestTest.java +++ b/src/test/java/com/auth0/net/MultipartRequestTest.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.mockwebserver.RecordedRequest; +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -360,6 +361,51 @@ public void shouldParseRateLimitsHeaders() throws Exception { assertThat(rateLimitException.getReset(), is(5L)); } + @Test + public void shouldParseRateLimitsWithAllHeaders() throws Exception { + MultipartRequest request = new MultipartRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.POST, listType); + request.addPart("non_empty", "body"); + server.rateLimitReachedResponse(100, 10, 5, getTokenQuotaString(), getTokenQuotaString(), 1000); + Exception exception = null; + try { + request.execute().getBody(); + server.takeRequest(); + } catch (Exception e) { + exception = e; + } + assertThat(exception, is(notNullValue())); + assertThat(exception, is(instanceOf(RateLimitException.class))); + assertThat(exception.getCause(), is(nullValue())); + assertThat(exception.getMessage(), is("Request failed with status code 429: Rate limit reached")); + RateLimitException rateLimitException = (RateLimitException) exception; + assertThat(rateLimitException.getDescription(), is("Rate limit reached")); + assertThat(rateLimitException.getError(), is(nullValue())); + assertThat(rateLimitException.getValue("non_existing_key"), is(nullValue())); + assertThat(rateLimitException.getStatusCode(), is(429)); + assertThat(rateLimitException.getLimit(), is(100L)); + assertThat(rateLimitException.getRemaining(), is(10L)); + assertThat(rateLimitException.getReset(), is(5L)); + assertThat(rateLimitException.getRetryAfter(), is(1000L)); + assertThat(rateLimitException.getClientQuotaLimit(), is(notNullValue())); + assertThat(rateLimitException.getClientQuotaLimit().getPerHour(), is(notNullValue())); + assertThat(rateLimitException.getClientQuotaLimit().getPerDay(), is(notNullValue())); + assertThat(rateLimitException.getClientQuotaLimit().getPerHour().getQuota(), is(100)); + assertThat(rateLimitException.getClientQuotaLimit().getPerHour().getRemaining(), is(80)); + assertThat(rateLimitException.getClientQuotaLimit().getPerHour().getResetAfter(), is(3600)); + assertThat(rateLimitException.getClientQuotaLimit().getPerDay().getQuota(), is(100)); + assertThat(rateLimitException.getClientQuotaLimit().getPerDay().getRemaining(), is(90)); + assertThat(rateLimitException.getClientQuotaLimit().getPerDay().getResetAfter(), is(86400)); + assertThat(rateLimitException.getOrganizationQuotaLimit(), is(notNullValue())); + assertThat(rateLimitException.getOrganizationQuotaLimit().getPerHour(), is(notNullValue())); + assertThat(rateLimitException.getOrganizationQuotaLimit().getPerDay(), is(notNullValue())); + assertThat(rateLimitException.getOrganizationQuotaLimit().getPerHour().getQuota(), is(100)); + assertThat(rateLimitException.getOrganizationQuotaLimit().getPerHour().getRemaining(), is(80)); + assertThat(rateLimitException.getOrganizationQuotaLimit().getPerHour().getResetAfter(), is(3600)); + assertThat(rateLimitException.getOrganizationQuotaLimit().getPerDay().getQuota(), is(100)); + assertThat(rateLimitException.getOrganizationQuotaLimit().getPerDay().getRemaining(), is(90)); + assertThat(rateLimitException.getOrganizationQuotaLimit().getPerDay().getResetAfter(), is(86400)); + } + @Test public void shouldDefaultRateLimitsHeadersWhenMissing() throws Exception { MultipartRequest request = new MultipartRequest<>(client, tokenProvider, server.getBaseUrl(), HttpMethod.POST, listType); @@ -384,6 +430,29 @@ public void shouldDefaultRateLimitsHeadersWhenMissing() throws Exception { assertThat(rateLimitException.getLimit(), is(-1L)); assertThat(rateLimitException.getRemaining(), is(-1L)); assertThat(rateLimitException.getReset(), is(-1L)); + assertThat(rateLimitException.getClientQuotaLimit(), Matchers.is(nullValue())); + assertThat(rateLimitException.getOrganizationQuotaLimit(), Matchers.is(nullValue())); } + public String getTokenQuotaString() { + TokenQuotaLimit perHourLimit = new TokenQuotaLimit(100, 80, 3600); + TokenQuotaLimit perDayLimit = new TokenQuotaLimit(100, 90, 86400); + + StringBuilder builder = new StringBuilder(); + + if (perHourLimit != null) { + builder.append(String.format("b=per_hour;q=%d;r=%d;t=%d", + perHourLimit.getQuota(), perHourLimit.getRemaining(), perHourLimit.getResetAfter())); + } + + if (perDayLimit != null) { + if (builder.length() > 0) { + builder.append(","); + } + builder.append(String.format("b=per_day;q=%d;r=%d;t=%d", + perDayLimit.getQuota(), perDayLimit.getRemaining(), perDayLimit.getResetAfter())); + } + + return builder.toString(); + } } diff --git a/src/test/java/com/auth0/utils/HttpResponseHeadersUtilsTest.java b/src/test/java/com/auth0/utils/HttpResponseHeadersUtilsTest.java new file mode 100644 index 000000000..77ddc8918 --- /dev/null +++ b/src/test/java/com/auth0/utils/HttpResponseHeadersUtilsTest.java @@ -0,0 +1,119 @@ +package com.auth0.utils; + +import com.auth0.net.TokenQuotaBucket; +import com.auth0.net.TokenQuotaLimit; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class HttpResponseHeadersUtilsTest { + + @Test + public void testGetClientQuotaLimitWithValidHeader() { + Map headers = new HashMap<>(); + headers.put("auth0-client-quota-limit", "per_hour;q=100;r=50;t=3600,per_day;q=1000;r=500;t=86400"); + + TokenQuotaBucket quotaBucket = HttpResponseHeadersUtils.getClientQuotaLimit(headers); + + assertNotNull(quotaBucket); + assertNotNull(quotaBucket.getPerHour()); + assertEquals(100, quotaBucket.getPerHour().getQuota()); + assertEquals(50, quotaBucket.getPerHour().getRemaining()); + assertEquals(3600, quotaBucket.getPerHour().getResetAfter()); + + assertNotNull(quotaBucket.getPerDay()); + assertEquals(1000, quotaBucket.getPerDay().getQuota()); + assertEquals(500, quotaBucket.getPerDay().getRemaining()); + assertEquals(86400, quotaBucket.getPerDay().getResetAfter()); + } + + @Test + public void testGetClientQuotaLimitWithMissingHeader() { + Map headers = new HashMap<>(); + + TokenQuotaBucket quotaBucket = HttpResponseHeadersUtils.getClientQuotaLimit(headers); + + assertNull(quotaBucket); + } + + @Test + public void testGetClientQuotaLimitWithOneBucketHeader() { + Map headers = new HashMap<>(); + headers.put("auth0-client-quota-limit", "per_hour;q=200;r=100;t=3600"); + + TokenQuotaBucket quotaBucket = HttpResponseHeadersUtils.getClientQuotaLimit(headers); + + assertNotNull(quotaBucket); + assertNotNull(quotaBucket.getPerHour()); + assertEquals(200, quotaBucket.getPerHour().getQuota()); + assertEquals(100, quotaBucket.getPerHour().getRemaining()); + assertEquals(3600, quotaBucket.getPerHour().getResetAfter()); + + assertNull(quotaBucket.getPerDay()); + } + + @Test + public void testGetOrganizationQuotaLimitWithValidHeader() { + Map headers = new HashMap<>(); + headers.put("auth0-organization-quota-limit", "per_hour;q=200;r=100;t=3600"); + + TokenQuotaBucket quotaBucket = HttpResponseHeadersUtils.getOrganizationQuotaLimit(headers); + + assertNotNull(quotaBucket); + assertNotNull(quotaBucket.getPerHour()); + assertEquals(200, quotaBucket.getPerHour().getQuota()); + assertEquals(100, quotaBucket.getPerHour().getRemaining()); + assertEquals(3600, quotaBucket.getPerHour().getResetAfter()); + + assertNull(quotaBucket.getPerDay()); + } + + @Test + public void testGetOrganizationQuotaLimitWithMissingHeader() { + Map headers = new HashMap<>(); + + TokenQuotaBucket quotaBucket = HttpResponseHeadersUtils.getOrganizationQuotaLimit(headers); + + assertNull(quotaBucket); + } + + @Test + public void testParseQuotaWithValidInput() { + String quotaHeader = "per_hour;q=300;r=150;t=3600,per_day;q=3000;r=1500;t=86400"; + + TokenQuotaBucket quotaBucket = HttpResponseHeadersUtils.parseQuota(quotaHeader); + + assertNotNull(quotaBucket); + assertNotNull(quotaBucket.getPerHour()); + assertEquals(300, quotaBucket.getPerHour().getQuota()); + assertEquals(150, quotaBucket.getPerHour().getRemaining()); + assertEquals(3600, quotaBucket.getPerHour().getResetAfter()); + + assertNotNull(quotaBucket.getPerDay()); + assertEquals(3000, quotaBucket.getPerDay().getQuota()); + assertEquals(1500, quotaBucket.getPerDay().getRemaining()); + assertEquals(86400, quotaBucket.getPerDay().getResetAfter()); + } + + @Test + public void testParseQuotaWithInvalidInput() { + String quotaHeader = "invalid_format"; + + TokenQuotaBucket quotaBucket = HttpResponseHeadersUtils.parseQuota(quotaHeader); + + assertNull(quotaBucket); + } + + @Test + public void testParseQuotaWithEmptyInput() { + String quotaHeader = ""; + + TokenQuotaBucket quotaBucket = HttpResponseHeadersUtils.parseQuota(quotaHeader); + + assertNull(quotaBucket); + } +} diff --git a/src/test/resources/mgmt/client.json b/src/test/resources/mgmt/client.json index a3395042b..967c9104c 100644 --- a/src/test/resources/mgmt/client.json +++ b/src/test/resources/mgmt/client.json @@ -91,5 +91,12 @@ ] }, "compliance_level": "fapi1_adv_pkj_par", - "require_proof_of_possession": true + "require_proof_of_possession": true, + "token_quota": { + "client_credentials": { + "per_day": 100, + "per_hour": 20, + "enforce": true + } + } } diff --git a/src/test/resources/mgmt/clients_list.json b/src/test/resources/mgmt/clients_list.json index e5a091aa4..208b5ff5a 100644 --- a/src/test/resources/mgmt/clients_list.json +++ b/src/test/resources/mgmt/clients_list.json @@ -72,6 +72,13 @@ "team_id": "9JA89QQLNQ", "app_bundle_identifier": "com.my.bundle.id" } + }, + "token_quota": { + "client_credentials": { + "per_day": 100, + "per_hour": 20, + "enforce": true + } } }, { diff --git a/src/test/resources/mgmt/organization.json b/src/test/resources/mgmt/organization.json index 3782c22c7..1453d8f94 100644 --- a/src/test/resources/mgmt/organization.json +++ b/src/test/resources/mgmt/organization.json @@ -17,5 +17,12 @@ "connection_id": "con-1", "assign_membership_on_login": false } - ] + ], + "token_quota": { + "client_credentials": { + "per_day": 100, + "per_hour": 20, + "enforce": true + } + } } diff --git a/src/test/resources/mgmt/organizations_list.json b/src/test/resources/mgmt/organizations_list.json index 504f7fd21..2b765439b 100644 --- a/src/test/resources/mgmt/organizations_list.json +++ b/src/test/resources/mgmt/organizations_list.json @@ -12,6 +12,13 @@ }, "metadata": { "key1": "val1" + }, + "token_quota": { + "client_credentials": { + "per_day": 100, + "per_hour": 20, + "enforce": true + } } }, { diff --git a/src/test/resources/mgmt/tenant.json b/src/test/resources/mgmt/tenant.json index 66195f797..0c7cd8b67 100644 --- a/src/test/resources/mgmt/tenant.json +++ b/src/test/resources/mgmt/tenant.json @@ -38,5 +38,21 @@ "remove_alg_from_jwks": true, "mtls": { "enable_endpoint_aliases": true + }, + "default_token_quota": { + "clients": { + "client_credentials": { + "per_day": 100, + "per_hour": 20, + "enforce": true + } + }, + "organizations": { + "client_credentials": { + "per_day": 100, + "per_hour": 20, + "enforce": true + } + } } }