Skip to content

Commit

Permalink
Merge pull request #67 from epam/integration-0.4.1
Browse files Browse the repository at this point in the history
feat: add path to claim "user roles" in app settings (#64)
  • Loading branch information
astsiapanay authored Nov 28, 2023
2 parents 9302a50 + ef5b0c7 commit a9c8d57
Show file tree
Hide file tree
Showing 6 changed files with 316 additions and 44 deletions.
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,26 @@ Static settings are used on startup and cannot be changed while application is r
* File specified in "AIDIAL_SETTINGS" environment variable.
* Default resource file: src/main/resources/aidial.settings.json.

|Setting |Default |Description
|-|-|-
|config.files |aidial.config.json |Config files with parts of the whole config.
|config.reload |60000 |Config reload interval in milliseconds.
|identityProvider.jwksUrl |- |Url to jwks provider.
|identityProvider.appName |dial |App name to search in "resource_access" claim of JWT token to check acess for deployments.
|identityProvider.loggingKey |- |User information to search in claims of JWT token.
|identityProvider.loggingSalt |- |Salt to hash user information for logging.
|identityProvider.cacheSize |10 |How many JWT tokens to cache.
|identityProvider.cacheExpiration |10 |How long to retain JWT token in cache.
|identityProvider.cacheExpirationUnit |MINUTES |Unit of cache expiration.
|vertx.* |- |Vertx settings.
|server.* |- |Vertx HTTP server settings for incoming requests.
|client.* |- |Vertx HTTP client settings for outbound requests.
|storage.provider |- |Specifies blob storage provider. Supported providers: s3, aws-s3, azureblob, google-cloud-storage
|storage.endpoint |- |Optional. Specifies endpoint url for s3 compatible storages
|storage.identity |- |Blob storage access key
|storage.credential |- |Blob storage secret key
|storage.bucket |- |Blob storage bucket
|storage.createBucket |false |Indicates whether bucket should be created on start-up
| Setting | Default |Description
|--------------------------------------|--------------------|-
| config.files | aidial.config.json |Config files with parts of the whole config.
| config.reload | 60000 |Config reload interval in milliseconds.
| identityProvider.jwksUrl | - |Url to jwks provider.
| identityProvider.rolePath | - |Path to the claim user roles in JWT token, e.g. `resource_access.chatbot-ui.roles` or just `roles`.
| identityProvider.loggingKey | - |User information to search in claims of JWT token.
| identityProvider.loggingSalt | - |Salt to hash user information for logging.
| identityProvider.cacheSize | 10 |How many JWT tokens to cache.
| identityProvider.cacheExpiration | 10 |How long to retain JWT token in cache.
| identityProvider.cacheExpirationUnit | MINUTES |Unit of cache expiration.
| vertx.* | - |Vertx settings.
| server.* | - |Vertx HTTP server settings for incoming requests.
| client.* | - |Vertx HTTP client settings for outbound requests.
| storage.provider | - |Specifies blob storage provider. Supported providers: s3, aws-s3, azureblob, google-cloud-storage
| storage.endpoint | - |Optional. Specifies endpoint url for s3 compatible storages
| storage.identity | - |Blob storage access key
| storage.credential | - |Blob storage secret key
| storage.bucket | - |Blob storage bucket
| storage.createBucket | false |Indicates whether bucket should be created on start-up

### Dynamic settings
Dynamic settings are stored in JSON files, specified via "config.files" static setting, and reloaded at interval, specified via "config.reload" static setting.
Expand Down
11 changes: 10 additions & 1 deletion src/main/java/com/epam/aidial/core/AiDial.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.epam.aidial.core;

import com.auth0.jwk.UrlJwkProvider;
import com.epam.aidial.core.config.ConfigStore;
import com.epam.aidial.core.config.FileConfigStore;
import com.epam.aidial.core.config.Storage;
Expand Down Expand Up @@ -30,6 +31,8 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -63,7 +66,13 @@ void start() throws Exception {
RateLimiter rateLimiter = new RateLimiter();
UpstreamBalancer upstreamBalancer = new UpstreamBalancer();

IdentityProvider identityProvider = new IdentityProvider(settings("identityProvider"), vertx);
IdentityProvider identityProvider = new IdentityProvider(settings("identityProvider"), vertx, jwksUrl -> {
try {
return new UrlJwkProvider(new URL(jwksUrl));
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
});
if (storage == null) {
Storage storageConfig = Json.decodeValue(settings("storage").toBuffer(), Storage.class);
storage = new BlobStorage(storageConfig);
Expand Down
51 changes: 30 additions & 21 deletions src/main/java/com/epam/aidial/core/security/IdentityProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkException;
import com.auth0.jwk.UrlJwkProvider;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
Expand All @@ -11,8 +11,6 @@
import io.vertx.core.json.JsonObject;
import lombok.extern.slf4j.Slf4j;

import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
Expand All @@ -23,15 +21,18 @@
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static java.util.Collections.EMPTY_LIST;

@Slf4j
public class IdentityProvider {

public static final ExtractedClaims CLAIMS_WITH_EMPTY_ROLES = new ExtractedClaims(null, Collections.emptyList(), null);

private final String appName;
private final String[] rolePath;

private final UrlJwkProvider jwkProvider;
private final JwkProvider jwkProvider;

private final ConcurrentHashMap<String, Future<JwkResult>> cache = new ConcurrentHashMap<>();

Expand All @@ -48,15 +49,15 @@ public class IdentityProvider {

private final long negativeCacheExpirationMs;

public IdentityProvider(JsonObject settings, Vertx vertx) {
public IdentityProvider(JsonObject settings, Vertx vertx, Function<String, JwkProvider> jwkProviderSupplier) {
if (settings == null) {
throw new IllegalArgumentException("Identity provider settings are missed");
}
this.vertx = vertx;
positiveCacheExpirationMs = settings.getLong("positiveCacheExpirationMs", TimeUnit.MINUTES.toMillis(10));
negativeCacheExpirationMs = settings.getLong("negativeCacheExpirationMs", TimeUnit.SECONDS.toMillis(10));
String jwksUrl = Objects.requireNonNull(settings.getString("jwksUrl"), "jwksUrl is missed");
appName = Objects.requireNonNull(settings.getString("appName"), "appName is missed");
rolePath = Objects.requireNonNull(settings.getString("rolePath"), "rolePath is missed").split("\\.");

loggingKey = settings.getString("loggingKey");
if (loggingKey != null) {
Expand All @@ -65,11 +66,7 @@ public IdentityProvider(JsonObject settings, Vertx vertx) {
loggingSalt = null;
}

try {
jwkProvider = new UrlJwkProvider(new URL(jwksUrl));
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
jwkProvider = jwkProviderSupplier.apply(jwksUrl);

try {
sha256Digest = MessageDigest.getInstance("SHA-256");
Expand All @@ -93,16 +90,28 @@ private void evictExpiredJwks() {

@SuppressWarnings("unchecked")
private List<String> extractUserRoles(DecodedJWT token) {
Map<String, Object> resourceAccess = token.getClaim("resource_access").asMap();
if (resourceAccess == null) {
return Collections.emptyList();
if (rolePath.length == 1) {
return token.getClaim(rolePath[0]).asList(String.class);
}
Map<String, Object> app = (Map<String, Object>) resourceAccess.get(appName);
if (app == null) {
return Collections.emptyList();
Map<String, Object> claim = token.getClaim(rolePath[0]).asMap();
for (int i = 1; i < rolePath.length; i++) {
Object next = claim.get(rolePath[i]);
if (next == null) {
return EMPTY_LIST;
}
if (i == rolePath.length - 1) {
if (next instanceof List) {
return (List<String>) next;
}
} else {
if (next instanceof Map) {
claim = (Map<String, Object>) next;
} else {
return EMPTY_LIST;
}
}
}
List<String> roles = (List<String>) app.get("roles");
return roles == null ? Collections.emptyList() : roles;
return EMPTY_LIST;
}

private DecodedJWT decodeJwtToken(String encodedToken) {
Expand Down Expand Up @@ -167,7 +176,7 @@ private String extractUserHash(DecodedJWT decodedJwt) {
public Future<ExtractedClaims> extractClaims(String authHeader, boolean isJwtMustBeVerified) {
try {
if (authHeader == null) {
return Future.succeededFuture();
return isJwtMustBeVerified ? Future.failedFuture(new IllegalArgumentException("Token is missed")) : Future.succeededFuture();
}
// Take the 1st authorization parameter from the header value:
// Authorization: <auth-scheme> <authorization-parameters>
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/aidial.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
},
"identityProvider": {
"jwksUrl": "http://fakeJwksUrl:8080",
"appName": "dial"
"rolePath": "roles"
},
"storage": {
"provider" : "s3",
Expand Down
Loading

0 comments on commit a9c8d57

Please sign in to comment.