mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2026-01-06 12:36:56 +00:00
* Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts * Fix #4637: Support JWT Token generation for bot accounts
This commit is contained in:
parent
1c68748618
commit
4531190af6
@ -64,6 +64,7 @@ import org.openmetadata.catalog.security.Authorizer;
|
||||
import org.openmetadata.catalog.security.AuthorizerConfiguration;
|
||||
import org.openmetadata.catalog.security.NoopAuthorizer;
|
||||
import org.openmetadata.catalog.security.NoopFilter;
|
||||
import org.openmetadata.catalog.security.jwt.JWTTokenGenerator;
|
||||
import org.openmetadata.catalog.security.policyevaluator.PolicyEvaluator;
|
||||
import org.openmetadata.catalog.security.policyevaluator.RoleEvaluator;
|
||||
import org.openmetadata.catalog.slack.SlackPublisherConfiguration;
|
||||
@ -98,6 +99,9 @@ public class CatalogApplication extends Application<CatalogApplicationConfig> {
|
||||
// Configure the Fernet instance
|
||||
Fernet.getInstance().setFernetKey(catalogConfig);
|
||||
|
||||
// Instantiate JWT Token Generator
|
||||
JWTTokenGenerator.getInstance().init(catalogConfig.getJwtTokenConfiguration());
|
||||
|
||||
// Set the Database type for choosing correct queries from annotations
|
||||
jdbi.getConfig(SqlObjects.class)
|
||||
.setSqlLocator(new ConnectionAwareAnnotationSqlLocator(catalogConfig.getDataSourceFactory().getDriverClass()));
|
||||
|
||||
@ -30,6 +30,7 @@ import org.openmetadata.catalog.fernet.FernetConfiguration;
|
||||
import org.openmetadata.catalog.migration.MigrationConfiguration;
|
||||
import org.openmetadata.catalog.security.AuthenticationConfiguration;
|
||||
import org.openmetadata.catalog.security.AuthorizerConfiguration;
|
||||
import org.openmetadata.catalog.security.jwt.JWTTokenConfiguration;
|
||||
import org.openmetadata.catalog.slack.SlackPublisherConfiguration;
|
||||
|
||||
public class CatalogApplicationConfig extends Configuration {
|
||||
@ -55,6 +56,11 @@ public class CatalogApplicationConfig extends Configuration {
|
||||
@Setter
|
||||
private AuthenticationConfiguration authenticationConfiguration;
|
||||
|
||||
@JsonProperty("jwtTokenConfiguration")
|
||||
@Getter
|
||||
@Setter
|
||||
private JWTTokenConfiguration jwtTokenConfiguration;
|
||||
|
||||
@JsonProperty("elasticsearch")
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
@ -20,6 +20,7 @@ import org.openmetadata.catalog.security.client.AzureSSOClientConfig;
|
||||
import org.openmetadata.catalog.security.client.CustomOIDCSSOClientConfig;
|
||||
import org.openmetadata.catalog.security.client.GoogleSSOClientConfig;
|
||||
import org.openmetadata.catalog.security.client.OktaSSOClientConfig;
|
||||
import org.openmetadata.catalog.security.client.OpenMetadataJWTClientConfig;
|
||||
|
||||
public class AuthConfiguration {
|
||||
|
||||
@ -32,4 +33,6 @@ public class AuthConfiguration {
|
||||
@Getter @Setter private AzureSSOClientConfig azure;
|
||||
|
||||
@Getter @Setter private CustomOIDCSSOClientConfig customOidc;
|
||||
|
||||
@Getter @Setter private OpenMetadataJWTClientConfig openMetadataJWTClientConfig;
|
||||
}
|
||||
|
||||
@ -26,11 +26,13 @@ import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jdbi.v3.sqlobject.transaction.Transaction;
|
||||
import org.openmetadata.catalog.Entity;
|
||||
import org.openmetadata.catalog.entity.teams.AuthenticationMechanism;
|
||||
import org.openmetadata.catalog.entity.teams.Team;
|
||||
import org.openmetadata.catalog.entity.teams.User;
|
||||
import org.openmetadata.catalog.exception.CatalogExceptionMessage;
|
||||
import org.openmetadata.catalog.jdbi3.EntityRepository.EntityUpdater;
|
||||
import org.openmetadata.catalog.resources.teams.UserResource;
|
||||
import org.openmetadata.catalog.teams.authn.JWTAuthMechanism;
|
||||
import org.openmetadata.catalog.type.ChangeDescription;
|
||||
import org.openmetadata.catalog.type.EntityReference;
|
||||
import org.openmetadata.catalog.type.Include;
|
||||
@ -38,6 +40,7 @@ import org.openmetadata.catalog.type.Relationship;
|
||||
import org.openmetadata.catalog.util.EntityInterface;
|
||||
import org.openmetadata.catalog.util.EntityUtil;
|
||||
import org.openmetadata.catalog.util.EntityUtil.Fields;
|
||||
import org.openmetadata.catalog.util.JsonUtils;
|
||||
|
||||
@Slf4j
|
||||
public class UserRepository extends EntityRepository<User> {
|
||||
@ -131,6 +134,8 @@ public class UserRepository extends EntityRepository<User> {
|
||||
user.setOwns(fields.contains("owns") ? getOwns(user) : null);
|
||||
user.setFollows(fields.contains("follows") ? getFollows(user) : null);
|
||||
user.setRoles(fields.contains("roles") ? getRoles(user) : null);
|
||||
user.setAuthenticationMechanism(
|
||||
fields.contains("authenticationMechanism") ? user.getAuthenticationMechanism() : null);
|
||||
return user.withInheritedRoles(fields.contains("roles") ? getInheritedRoles(user) : null);
|
||||
}
|
||||
|
||||
@ -363,9 +368,9 @@ public class UserRepository extends EntityRepository<User> {
|
||||
recordChange("isBot", origUser.getIsBot(), updatedUser.getIsBot());
|
||||
recordChange("isAdmin", origUser.getIsAdmin(), updatedUser.getIsAdmin());
|
||||
recordChange("email", origUser.getEmail(), updatedUser.getEmail());
|
||||
|
||||
// Add inherited roles to the entity after update
|
||||
updatedUser.setInheritedRoles(getInheritedRoles(updatedUser));
|
||||
updateAuthenticationMechanism(origUser, updatedUser);
|
||||
}
|
||||
|
||||
private void updateRoles(User origUser, User updatedUser) throws IOException {
|
||||
@ -399,5 +404,28 @@ public class UserRepository extends EntityRepository<User> {
|
||||
List<EntityReference> deleted = new ArrayList<>();
|
||||
recordListChange("teams", origTeams, updatedTeams, added, deleted, EntityUtil.entityReferenceMatch);
|
||||
}
|
||||
|
||||
private void updateAuthenticationMechanism(User origUser, User updatedUser) throws IOException {
|
||||
AuthenticationMechanism origAuthMechanism = origUser.getAuthenticationMechanism();
|
||||
AuthenticationMechanism updatedAuthMechanism = updatedUser.getAuthenticationMechanism();
|
||||
if (origAuthMechanism == null && updatedAuthMechanism != null) {
|
||||
recordChange(
|
||||
"authenticationMechanism", origUser.getAuthenticationMechanism(), updatedUser.getAuthenticationMechanism());
|
||||
} else if (origAuthMechanism != null
|
||||
&& updatedAuthMechanism != null
|
||||
&& origAuthMechanism.getConfig() != null
|
||||
&& updatedAuthMechanism.getConfig() != null) {
|
||||
JWTAuthMechanism origJwtAuthMechanism =
|
||||
JsonUtils.convertValue(origAuthMechanism.getConfig(), JWTAuthMechanism.class);
|
||||
JWTAuthMechanism updatedJwtAuthMechanism =
|
||||
JsonUtils.convertValue(updatedAuthMechanism.getConfig(), JWTAuthMechanism.class);
|
||||
if (!origJwtAuthMechanism.getJWTToken().equals(updatedJwtAuthMechanism.getJWTToken())) {
|
||||
recordChange(
|
||||
"authenticationMechanism",
|
||||
origUser.getAuthenticationMechanism(),
|
||||
updatedUser.getAuthenticationMechanism());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,8 @@ import org.openmetadata.catalog.resources.Collection;
|
||||
import org.openmetadata.catalog.sandbox.SandboxConfiguration;
|
||||
import org.openmetadata.catalog.security.AuthenticationConfiguration;
|
||||
import org.openmetadata.catalog.security.AuthorizerConfiguration;
|
||||
import org.openmetadata.catalog.security.jwt.JWKSResponse;
|
||||
import org.openmetadata.catalog.security.jwt.JWTTokenGenerator;
|
||||
|
||||
@Path("/v1/config")
|
||||
@Api(value = "Get configuration")
|
||||
@ -35,9 +37,11 @@ import org.openmetadata.catalog.security.AuthorizerConfiguration;
|
||||
@Collection(name = "config")
|
||||
public class ConfigResource {
|
||||
private final CatalogApplicationConfig catalogApplicationConfig;
|
||||
private final JWTTokenGenerator jwtTokenGenerator;
|
||||
|
||||
public ConfigResource(CatalogApplicationConfig catalogApplicationConfig) {
|
||||
this.catalogApplicationConfig = catalogApplicationConfig;
|
||||
this.jwtTokenGenerator = JWTTokenGenerator.getInstance();
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -125,4 +129,19 @@ public class ConfigResource {
|
||||
}
|
||||
return airflowConfigurationForAPI;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path(("/jwks"))
|
||||
@Operation(
|
||||
summary = "Get JWKS public key",
|
||||
tags = "general",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "JWKS public key",
|
||||
content = @Content(mediaType = "application/json", schema = @Schema(implementation = JWKSResponse.class)))
|
||||
})
|
||||
public JWKSResponse getJWKSResponse() {
|
||||
return jwtTokenGenerator.getJWKSResponse();
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,8 +52,10 @@ import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.SecurityContext;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.openmetadata.catalog.Entity;
|
||||
import org.openmetadata.catalog.api.teams.CreateUser;
|
||||
import org.openmetadata.catalog.entity.teams.AuthenticationMechanism;
|
||||
import org.openmetadata.catalog.entity.teams.User;
|
||||
import org.openmetadata.catalog.jdbi3.CollectionDAO;
|
||||
import org.openmetadata.catalog.jdbi3.ListFilter;
|
||||
@ -63,9 +65,12 @@ import org.openmetadata.catalog.resources.Collection;
|
||||
import org.openmetadata.catalog.resources.EntityResource;
|
||||
import org.openmetadata.catalog.security.Authorizer;
|
||||
import org.openmetadata.catalog.security.SecurityUtil;
|
||||
import org.openmetadata.catalog.security.jwt.JWTTokenGenerator;
|
||||
import org.openmetadata.catalog.teams.authn.JWTAuthMechanism;
|
||||
import org.openmetadata.catalog.type.EntityHistory;
|
||||
import org.openmetadata.catalog.type.Include;
|
||||
import org.openmetadata.catalog.util.EntityUtil.Fields;
|
||||
import org.openmetadata.catalog.util.JsonUtils;
|
||||
import org.openmetadata.catalog.util.RestUtil;
|
||||
import org.openmetadata.catalog.util.ResultList;
|
||||
|
||||
@ -77,6 +82,8 @@ import org.openmetadata.catalog.util.ResultList;
|
||||
@Collection(name = "users")
|
||||
public class UserResource extends EntityResource<User, UserRepository> {
|
||||
public static final String COLLECTION_PATH = "v1/users/";
|
||||
public static final String USER_PROTECTED_FIELDS = "authenticationMechanism";
|
||||
private JWTTokenGenerator jwtTokenGenerator;
|
||||
|
||||
@Override
|
||||
public User addHref(UriInfo uriInfo, User user) {
|
||||
@ -90,6 +97,8 @@ public class UserResource extends EntityResource<User, UserRepository> {
|
||||
|
||||
public UserResource(CollectionDAO dao, Authorizer authorizer) {
|
||||
super(User.class, new UserRepository(dao), authorizer);
|
||||
jwtTokenGenerator = JWTTokenGenerator.getInstance();
|
||||
allowedFields.remove(USER_PROTECTED_FIELDS);
|
||||
}
|
||||
|
||||
public static class UserList extends ResultList<User> {
|
||||
@ -344,6 +353,106 @@ public class UserResource extends EntityResource<User, UserRepository> {
|
||||
return response.toResponse();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/generateToken/{id}")
|
||||
@Operation(
|
||||
summary = "Generate JWT Token for a Bot User",
|
||||
tags = "users",
|
||||
description = "Generate JWT Token for a Bot User.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "The user ",
|
||||
content =
|
||||
@Content(mediaType = "application/json", schema = @Schema(implementation = JWTAuthMechanism.class))),
|
||||
@ApiResponse(responseCode = "400", description = "Bad request")
|
||||
})
|
||||
public JWTAuthMechanism generateToken(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@PathParam("id") String id,
|
||||
JWTAuthMechanism jwtAuthMechanism)
|
||||
throws IOException {
|
||||
|
||||
User user = dao.get(uriInfo, id, Fields.EMPTY_FIELDS);
|
||||
if (!user.getIsBot()) {
|
||||
throw new IllegalArgumentException("Generating JWT token is only supported for bot users");
|
||||
}
|
||||
SecurityUtil.authorizeAdmin(authorizer, securityContext, ADMIN);
|
||||
String token = jwtTokenGenerator.generateJWTToken(user, jwtAuthMechanism.getJWTTokenExpiry());
|
||||
jwtAuthMechanism.setJWTToken(token);
|
||||
AuthenticationMechanism authenticationMechanism =
|
||||
new AuthenticationMechanism().withConfig(jwtAuthMechanism).withAuthType(AuthenticationMechanism.AuthType.JWT);
|
||||
user.setAuthenticationMechanism(authenticationMechanism);
|
||||
User updatedUser = dao.createOrUpdate(uriInfo, user).getEntity();
|
||||
jwtAuthMechanism =
|
||||
JsonUtils.convertValue(updatedUser.getAuthenticationMechanism().getConfig(), JWTAuthMechanism.class);
|
||||
return jwtAuthMechanism;
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/revokeToken/{id}")
|
||||
@Operation(
|
||||
summary = "Revoke JWT Token for a Bot User",
|
||||
tags = "users",
|
||||
description = "Revoke JWT Token for a Bot User.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "The user ",
|
||||
content =
|
||||
@Content(mediaType = "application/json", schema = @Schema(implementation = JWTAuthMechanism.class))),
|
||||
@ApiResponse(responseCode = "400", description = "Bad request")
|
||||
})
|
||||
public Response revokeToken(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @PathParam("id") String id)
|
||||
throws IOException {
|
||||
|
||||
User user = dao.get(uriInfo, id, Fields.EMPTY_FIELDS);
|
||||
if (!user.getIsBot()) {
|
||||
throw new IllegalArgumentException("Generating JWT token is only supported for bot users");
|
||||
}
|
||||
SecurityUtil.authorizeAdmin(authorizer, securityContext, ADMIN);
|
||||
JWTAuthMechanism jwtAuthMechanism = new JWTAuthMechanism().withJWTToken(StringUtils.EMPTY);
|
||||
AuthenticationMechanism authenticationMechanism =
|
||||
new AuthenticationMechanism().withConfig(jwtAuthMechanism).withAuthType(AuthenticationMechanism.AuthType.JWT);
|
||||
user.setAuthenticationMechanism(authenticationMechanism);
|
||||
RestUtil.PutResponse<User> response = dao.createOrUpdate(uriInfo, user);
|
||||
addHref(uriInfo, response.getEntity());
|
||||
return response.toResponse();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/token/{id}")
|
||||
@Operation(
|
||||
summary = "Get JWT Token for a Bot User",
|
||||
tags = "users",
|
||||
description = "Get JWT Token for a Bot User.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "The user ",
|
||||
content =
|
||||
@Content(mediaType = "application/json", schema = @Schema(implementation = JWTAuthMechanism.class))),
|
||||
@ApiResponse(responseCode = "400", description = "Bad request")
|
||||
})
|
||||
public JWTAuthMechanism getToken(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @PathParam("id") String id)
|
||||
throws IOException {
|
||||
|
||||
User user = dao.get(uriInfo, id, new Fields(List.of("authenticationMechanism")));
|
||||
if (!user.getIsBot()) {
|
||||
throw new IllegalArgumentException("JWT token is only supported for bot users");
|
||||
}
|
||||
SecurityUtil.authorizeAdmin(authorizer, securityContext, ADMIN);
|
||||
AuthenticationMechanism authenticationMechanism = user.getAuthenticationMechanism();
|
||||
if (authenticationMechanism.getConfig() != null
|
||||
&& authenticationMechanism.getAuthType() == AuthenticationMechanism.AuthType.JWT) {
|
||||
return JsonUtils.convertValue(authenticationMechanism.getConfig(), JWTAuthMechanism.class);
|
||||
}
|
||||
return new JWTAuthMechanism();
|
||||
}
|
||||
|
||||
@PATCH
|
||||
@Path("/{id}")
|
||||
@Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
|
||||
|
||||
@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import io.dropwizard.util.Strings;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Calendar;
|
||||
@ -36,13 +37,21 @@ import javax.ws.rs.core.UriInfo;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.openmetadata.catalog.Entity;
|
||||
import org.openmetadata.catalog.entity.teams.AuthenticationMechanism;
|
||||
import org.openmetadata.catalog.entity.teams.User;
|
||||
import org.openmetadata.catalog.jdbi3.EntityRepository;
|
||||
import org.openmetadata.catalog.security.auth.CatalogSecurityContext;
|
||||
import org.openmetadata.catalog.teams.authn.JWTAuthMechanism;
|
||||
import org.openmetadata.catalog.util.EntityUtil;
|
||||
import org.openmetadata.catalog.util.JsonUtils;
|
||||
|
||||
@Slf4j
|
||||
@Provider
|
||||
public class JwtFilter implements ContainerRequestFilter {
|
||||
public static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
public static final String TOKEN_PREFIX = "Bearer";
|
||||
public static final String BOT_CLAIM = "isBot";
|
||||
|
||||
private List<String> jwtPrincipalClaims;
|
||||
private JwkProvider jwkProvider;
|
||||
@ -89,7 +98,9 @@ public class JwtFilter implements ContainerRequestFilter {
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if (jwt.getExpiresAt().before(Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTime())) {
|
||||
// if the expiresAt set to null, treat it as never expiring token
|
||||
if (jwt.getExpiresAt() != null
|
||||
&& jwt.getExpiresAt().before(Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTime())) {
|
||||
throw new AuthenticationException("Expired token!");
|
||||
}
|
||||
|
||||
@ -121,7 +132,10 @@ public class JwtFilter implements ContainerRequestFilter {
|
||||
() ->
|
||||
new AuthenticationException(
|
||||
"Invalid JWT token, none of the following claims are present " + jwtPrincipalClaims));
|
||||
|
||||
// validate bot token
|
||||
if (jwt.getClaims().containsKey(BOT_CLAIM) && jwt.getClaims().get(BOT_CLAIM).asBoolean()) {
|
||||
validateBotToken(tokenFromHeader, userName);
|
||||
}
|
||||
// Setting Security Context
|
||||
CatalogPrincipal catalogPrincipal = new CatalogPrincipal(userName);
|
||||
String scheme = requestContext.getUriInfo().getRequestUri().getScheme();
|
||||
@ -143,4 +157,18 @@ public class JwtFilter implements ContainerRequestFilter {
|
||||
}
|
||||
throw new AuthenticationException("Not Authorized! Token not present");
|
||||
}
|
||||
|
||||
private void validateBotToken(String tokenFromHeader, String userName) throws IOException {
|
||||
EntityRepository<User> userRepository = Entity.getEntityRepository(Entity.USER);
|
||||
User user = userRepository.getByName(null, userName, new EntityUtil.Fields(List.of("authenticationMechanism")));
|
||||
AuthenticationMechanism authenticationMechanism = user.getAuthenticationMechanism();
|
||||
if (authenticationMechanism != null) {
|
||||
JWTAuthMechanism jwtAuthMechanism =
|
||||
JsonUtils.convertValue(authenticationMechanism.getConfig(), JWTAuthMechanism.class);
|
||||
if (tokenFromHeader.equals(jwtAuthMechanism.getJWTToken())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new AuthenticationException("Not Authorized! Invalid Token");
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
package org.openmetadata.catalog.security.jwt;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class JWKSKey {
|
||||
@NotEmpty @Getter @Setter private String kty;
|
||||
@NotEmpty @Getter @Setter private String kid;
|
||||
@NotEmpty @Getter @Setter private String n;
|
||||
@NotEmpty @Getter @Setter private String e;
|
||||
@NotEmpty @Getter @Setter private String alg = "RS256";
|
||||
@NotEmpty @Getter @Setter private String use = "sig";
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package org.openmetadata.catalog.security.jwt;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class JWKSResponse {
|
||||
@JsonProperty("keys")
|
||||
@NotEmpty
|
||||
@Getter
|
||||
@Setter
|
||||
List<JWKSKey> jwsKeys;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package org.openmetadata.catalog.security.jwt;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class JWTTokenConfiguration {
|
||||
@Getter @Setter private String RSAPublicKeyFilePath;
|
||||
@Getter @Setter private String RSAPrivateKeyFilePath;
|
||||
@Getter @Setter private String JWTIssuer;
|
||||
@Getter @Setter private String keyId;
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
package org.openmetadata.catalog.security.jwt;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.exceptions.JWTCreationException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.openmetadata.catalog.entity.teams.User;
|
||||
import org.openmetadata.catalog.teams.authn.JWTTokenExpiry;
|
||||
|
||||
@Slf4j
|
||||
public class JWTTokenGenerator {
|
||||
private static volatile JWTTokenGenerator instance;
|
||||
private RSAPrivateKey privateKey;
|
||||
private RSAPublicKey publicKey;
|
||||
private String issuer;
|
||||
private String kid;
|
||||
|
||||
private JWTTokenGenerator() {
|
||||
if (instance != null) {
|
||||
throw new RuntimeException("Use getInstance() method to get the single instance of this class");
|
||||
}
|
||||
}
|
||||
|
||||
public static JWTTokenGenerator getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (JWTTokenGenerator.class) {
|
||||
if (instance == null) {
|
||||
instance = new JWTTokenGenerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void init(JWTTokenConfiguration jwtTokenConfiguration) {
|
||||
try {
|
||||
if (jwtTokenConfiguration.getRSAPrivateKeyFilePath() != null
|
||||
&& !jwtTokenConfiguration.getRSAPrivateKeyFilePath().isEmpty()
|
||||
&& jwtTokenConfiguration.getRSAPublicKeyFilePath() != null
|
||||
&& !jwtTokenConfiguration.getRSAPublicKeyFilePath().isEmpty()) {
|
||||
byte[] privateKeyBytes = Files.readAllBytes(Paths.get(jwtTokenConfiguration.getRSAPrivateKeyFilePath()));
|
||||
PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(privateKeyBytes);
|
||||
KeyFactory privateKF = KeyFactory.getInstance("RSA");
|
||||
privateKey = (RSAPrivateKey) privateKF.generatePrivate(privateSpec);
|
||||
byte[] publicKeyBytes = Files.readAllBytes(Paths.get(jwtTokenConfiguration.getRSAPublicKeyFilePath()));
|
||||
X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKeyBytes);
|
||||
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||
publicKey = (RSAPublicKey) kf.generatePublic(spec);
|
||||
issuer = jwtTokenConfiguration.getJWTIssuer();
|
||||
kid = jwtTokenConfiguration.getKeyId();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Failed to initialize JWTTokenGenerator ", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public RSAPublicKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public String generateJWTToken(User user, JWTTokenExpiry expiry) {
|
||||
try {
|
||||
Algorithm algorithm = Algorithm.RSA256(null, privateKey);
|
||||
Date expires = getExpiryDate(expiry);
|
||||
return JWT.create()
|
||||
.withIssuer(issuer)
|
||||
.withKeyId(kid)
|
||||
.withClaim("sub", user.getName())
|
||||
.withClaim("email", user.getEmail())
|
||||
.withClaim("isBot", true)
|
||||
.withIssuedAt(new Date(System.currentTimeMillis()))
|
||||
.withExpiresAt(expires)
|
||||
.sign(algorithm);
|
||||
} catch (Exception e) {
|
||||
throw new JWTCreationException("Failed to generate JWT Token. Please check your OpenMetadata Configuration.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Date getExpiryDate(JWTTokenExpiry jwtTokenExpiry) {
|
||||
LocalDateTime expiryDate;
|
||||
switch (jwtTokenExpiry) {
|
||||
case Seven:
|
||||
expiryDate = LocalDateTime.now().plusDays(7);
|
||||
break;
|
||||
case Thirty:
|
||||
expiryDate = LocalDateTime.now().plusDays(30);
|
||||
break;
|
||||
case Sixty:
|
||||
expiryDate = LocalDateTime.now().plusDays(60);
|
||||
break;
|
||||
case Ninety:
|
||||
expiryDate = LocalDateTime.now().plusDays(90);
|
||||
break;
|
||||
case Unlimited:
|
||||
default:
|
||||
expiryDate = null;
|
||||
}
|
||||
return expiryDate != null ? Date.from(expiryDate.atZone(ZoneId.systemDefault()).toInstant()) : null;
|
||||
}
|
||||
|
||||
public JWKSResponse getJWKSResponse() {
|
||||
JWKSResponse jwksResponse = new JWKSResponse();
|
||||
JWKSKey jwksKey = new JWKSKey();
|
||||
if (publicKey != null) {
|
||||
jwksKey.setKid(kid);
|
||||
jwksKey.setKty(publicKey.getAlgorithm());
|
||||
jwksKey.setN(Base64.getUrlEncoder().encodeToString(publicKey.getModulus().toByteArray()));
|
||||
jwksKey.setE(Base64.getUrlEncoder().encodeToString(publicKey.getPublicExponent().toByteArray()));
|
||||
}
|
||||
jwksResponse.setJwsKeys(List.of(jwksKey));
|
||||
return jwksResponse;
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ import org.openmetadata.catalog.security.client.AzureSSOClientConfig;
|
||||
import org.openmetadata.catalog.security.client.CustomOIDCSSOClientConfig;
|
||||
import org.openmetadata.catalog.security.client.GoogleSSOClientConfig;
|
||||
import org.openmetadata.catalog.security.client.OktaSSOClientConfig;
|
||||
import org.openmetadata.catalog.security.client.OpenMetadataJWTClientConfig;
|
||||
import org.openmetadata.catalog.services.connections.metadata.OpenMetadataServerConnection;
|
||||
import org.openmetadata.catalog.services.connections.metadata.OpenMetadataServerConnection.AuthProvider;
|
||||
|
||||
@ -27,6 +28,7 @@ public final class OpenMetadataClientSecurityUtil {
|
||||
public static final String AUTHORITY = "authority";
|
||||
public static final String CLIENT_SECRET = "clientSecret";
|
||||
public static final String SECRET_KEY = "secretKey";
|
||||
public static final String JWT_TOKEN = "jwtToken";
|
||||
|
||||
private OpenMetadataClientSecurityUtil() {
|
||||
/* Utility class with private constructor */
|
||||
@ -83,6 +85,11 @@ public final class OpenMetadataClientSecurityUtil {
|
||||
checkRequiredField("tokenEndpoint", customOIDCSSOClientConfig.getTokenEndpoint(), authProvider);
|
||||
openMetadataServerConnection.setSecurityConfig(customOIDCSSOClientConfig);
|
||||
break;
|
||||
case OPENMETADATA:
|
||||
OpenMetadataJWTClientConfig openMetadataJWTClientConfig = authConfig.getOpenMetadataJWTClientConfig();
|
||||
checkAuthConfig(openMetadataJWTClientConfig, authProvider);
|
||||
checkRequiredField(JWT_TOKEN, openMetadataJWTClientConfig.getJwtToken(), authProvider);
|
||||
openMetadataServerConnection.setSecurityConfig(openMetadataJWTClientConfig);
|
||||
case NO_AUTH:
|
||||
break;
|
||||
default:
|
||||
|
||||
@ -27,7 +27,15 @@
|
||||
"authProvider": {
|
||||
"description": "OpenMetadata Server Authentication Provider. Make sure configure same auth providers as the one configured on OpenMetadaata server.",
|
||||
"type": "string",
|
||||
"enum": ["no-auth", "azure", "google", "okta", "auth0", "custom-oidc"],
|
||||
"enum": [
|
||||
"no-auth",
|
||||
"azure",
|
||||
"google",
|
||||
"okta",
|
||||
"auth0",
|
||||
"custom-oidc",
|
||||
"openmetadata"
|
||||
],
|
||||
"default": "no-auth"
|
||||
},
|
||||
"securityConfig": {
|
||||
@ -47,6 +55,9 @@
|
||||
},
|
||||
{
|
||||
"$ref": "../../../../security/client/customOidcSSOClientConfig.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../../../../security/client/openMetadataJWTClientConfig.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/schema/entity/teams/authN/jwtAuth.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "JWTAuthMechanism",
|
||||
"description": "User/Bot JWTAuthMechanism.",
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.catalog.teams.authn.JWTAuthMechanism",
|
||||
"properties": {
|
||||
"JWTToken": {
|
||||
"description": "JWT Auth Token.",
|
||||
"type": "string"
|
||||
},
|
||||
"JWTTokenExpiry": {
|
||||
"javaType": "org.openmetadata.catalog.teams.authn.JWTTokenExpiry",
|
||||
"description": "JWT Auth Token expiration in days",
|
||||
"type": "string",
|
||||
"enum": ["7", "30", "60", "90", "Unlimited"],
|
||||
"javaEnums": [
|
||||
{
|
||||
"name": "Seven"
|
||||
},
|
||||
{
|
||||
"name": "Thirty"
|
||||
},
|
||||
{
|
||||
"name": "Sixty"
|
||||
},
|
||||
{
|
||||
"name": "Ninety"
|
||||
},
|
||||
{
|
||||
"name": "Unlimited"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["JWTToken", "JWTTokenExpiry"]
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/schema/entity/teams/authN/ssoAuth.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "SSOAuthMechanism",
|
||||
"description": "User/Bot SSOAuthN.",
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.catalog.teams.authn.SSOAuthMechanism",
|
||||
"properties": {
|
||||
"ssoServiceType": {
|
||||
"description": "Type of database service such as Amundsen, Atlas...",
|
||||
"type": "string",
|
||||
"enum": ["Google", "Okta", "CustomOIDC", "Auth0", "Azure"],
|
||||
"javaEnums": [
|
||||
{
|
||||
"name": "Google"
|
||||
},
|
||||
{
|
||||
"name": "Okta"
|
||||
},
|
||||
{
|
||||
"name": "Auth0"
|
||||
},
|
||||
{
|
||||
"name": "CustomOIDC"
|
||||
},
|
||||
{
|
||||
"name": "Azure"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@ -4,6 +4,28 @@
|
||||
"title": "User",
|
||||
"description": "This schema defines the User entity. A user can be part of 0 or more teams. A special type of user called Bot is used for automation. A user can be an owner of zero or more data assets. A user can also follow zero or more data assets.",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"authenticationMechanism": {
|
||||
"type": "object",
|
||||
"description": "User/Bot Authentication Mechanism.",
|
||||
"properties": {
|
||||
"config": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "./authN/ssoAuth.json"
|
||||
},
|
||||
{
|
||||
"$ref": "./authN/jwtAuth.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"authType": {
|
||||
"enum": ["JWT", "SSO"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique identifier that identifies a user entity instance.",
|
||||
@ -56,6 +78,9 @@
|
||||
"type": "boolean",
|
||||
"boolean": false
|
||||
},
|
||||
"authenticationMechanism": {
|
||||
"$ref": "#/definitions/authenticationMechanism"
|
||||
},
|
||||
"profile": {
|
||||
"description": "Profile of the user.",
|
||||
"$ref": "../../type/profile.json"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/security/client/auth0SSOClientConfig.json",
|
||||
"$id": "https://open-metadata.org/schema/security/client/auth0SSOClientConfig.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Auth0SSOClientConfig",
|
||||
"description": "Auth0 SSO client security configs.",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/security/client/customOidcSSOClientConfig.json",
|
||||
"$id": "https://open-metadata.org/schema/security/client/customOidcSSOClientConfig.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "CustomOIDCSSOClientConfig",
|
||||
"description": "Custom OIDC SSO client security configs.",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/security/client/googleSSOClientConfig.json",
|
||||
"$id": "https://open-metadata.org/schema/security/client/googleSSOClientConfig.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "GoogleSSOClientConfig",
|
||||
"description": "Google SSO client security configs.",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/security/client/oktaSSOClientConfig.json",
|
||||
"$id": "https://open-metadata.org/schema/security/client/oktaSSOClientConfig.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "OktaSSOClientConfig",
|
||||
"description": "Okta SSO client security configs.",
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/schema/security/client/openMetadataJWTClientConfig.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "openMetadataJWTClientConfig",
|
||||
"description": "openMetadataJWTClientConfig security configs.",
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.catalog.security.client.OpenMetadataJWTClientConfig",
|
||||
"properties": {
|
||||
"jwtToken": {
|
||||
"description": "OpenMetadata generated JWT token.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["jwtToken"]
|
||||
}
|
||||
@ -14,6 +14,7 @@
|
||||
package org.openmetadata.catalog.resources.config;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.openmetadata.catalog.util.TestUtils.TEST_AUTH_HEADERS;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@ -33,6 +34,8 @@ import org.openmetadata.catalog.CatalogApplicationTest;
|
||||
import org.openmetadata.catalog.airflow.AirflowConfigurationForAPI;
|
||||
import org.openmetadata.catalog.security.AuthenticationConfiguration;
|
||||
import org.openmetadata.catalog.security.AuthorizerConfiguration;
|
||||
import org.openmetadata.catalog.security.jwt.JWKSKey;
|
||||
import org.openmetadata.catalog.security.jwt.JWKSResponse;
|
||||
import org.openmetadata.catalog.util.TestUtils;
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
@ -70,4 +73,18 @@ class ConfigResourceTest extends CatalogApplicationTest {
|
||||
AirflowConfigurationForAPI auth = TestUtils.get(target, AirflowConfigurationForAPI.class, TEST_AUTH_HEADERS);
|
||||
assertEquals(config.getAirflowConfiguration().getApiEndpoint(), auth.getApiEndpoint());
|
||||
}
|
||||
|
||||
@Test
|
||||
void get_jwks_configs_200_OK() throws IOException {
|
||||
WebTarget target = getConfigResource("jwks");
|
||||
JWKSResponse auth = TestUtils.get(target, JWKSResponse.class, TEST_AUTH_HEADERS);
|
||||
assertNotNull(auth);
|
||||
assertEquals(auth.getJwsKeys().size(), 1);
|
||||
JWKSKey jwksKey = auth.getJwsKeys().get(0);
|
||||
assertEquals(jwksKey.getAlg(), "RS256");
|
||||
assertEquals(jwksKey.getUse(), "sig");
|
||||
assertEquals(jwksKey.getKty(), "RSA");
|
||||
assertNotNull(jwksKey.getN());
|
||||
assertNotNull(jwksKey.getE());
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,19 +44,29 @@ import static org.openmetadata.catalog.util.TestUtils.validateAlphabeticalOrderi
|
||||
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
|
||||
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
|
||||
|
||||
import com.auth0.jwk.JwkException;
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.exceptions.JWTDecodeException;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
import javax.ws.rs.client.WebTarget;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.client.HttpResponseException;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
@ -77,6 +87,10 @@ import org.openmetadata.catalog.resources.EntityResourceTest;
|
||||
import org.openmetadata.catalog.resources.databases.TableResourceTest;
|
||||
import org.openmetadata.catalog.resources.locations.LocationResourceTest;
|
||||
import org.openmetadata.catalog.resources.teams.UserResource.UserList;
|
||||
import org.openmetadata.catalog.security.AuthenticationException;
|
||||
import org.openmetadata.catalog.security.jwt.JWKSResponse;
|
||||
import org.openmetadata.catalog.teams.authn.JWTAuthMechanism;
|
||||
import org.openmetadata.catalog.teams.authn.JWTTokenExpiry;
|
||||
import org.openmetadata.catalog.type.ChangeDescription;
|
||||
import org.openmetadata.catalog.type.EntityReference;
|
||||
import org.openmetadata.catalog.type.FieldChange;
|
||||
@ -97,6 +111,7 @@ public class UserResourceTest extends EntityResourceTest<User, CreateUser> {
|
||||
public UserResourceTest() {
|
||||
super(Entity.USER, User.class, UserList.class, "users", UserResource.FIELDS);
|
||||
this.supportsAuthorizedMetadataOperations = false;
|
||||
this.supportsFieldsQueryParam = false;
|
||||
}
|
||||
|
||||
public void setupUsers(TestInfo test) throws HttpResponseException {
|
||||
@ -669,6 +684,59 @@ public class UserResourceTest extends EntityResourceTest<User, CreateUser> {
|
||||
entityNotFound("user", user.getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void put_generateToken_bot_user_200_ok(TestInfo test)
|
||||
throws HttpResponseException, MalformedURLException, JwkException {
|
||||
User user =
|
||||
createEntity(
|
||||
createRequest(test, 6)
|
||||
.withName("ingestion-bot-jwt")
|
||||
.withDisplayName("ingestion-bot-jwt")
|
||||
.withEmail("ingestion-bot-jwt@email.com")
|
||||
.withIsBot(true),
|
||||
authHeaders("ingestion-bot-jwt@email.com"));
|
||||
JWTAuthMechanism authMechanism = new JWTAuthMechanism().withJWTTokenExpiry(JWTTokenExpiry.Seven);
|
||||
TestUtils.put(
|
||||
getResource(String.format("users/generateToken/%s", user.getId())), authMechanism, OK, ADMIN_AUTH_HEADERS);
|
||||
user = getEntity(user.getId(), ADMIN_AUTH_HEADERS);
|
||||
assertNull(user.getAuthenticationMechanism());
|
||||
JWTAuthMechanism jwtAuthMechanism =
|
||||
TestUtils.get(
|
||||
getResource(String.format("users/token/%s", user.getId())), JWTAuthMechanism.class, ADMIN_AUTH_HEADERS);
|
||||
assertNotNull(jwtAuthMechanism.getJWTToken());
|
||||
DecodedJWT jwt = decodedJWT(jwtAuthMechanism.getJWTToken());
|
||||
Date date = jwt.getExpiresAt();
|
||||
long daysBetween = ((date.getTime() - jwt.getIssuedAt().getTime()) / (1000 * 60 * 60 * 24));
|
||||
assertTrue(daysBetween >= 6);
|
||||
assertEquals(jwt.getClaims().get("sub").asString(), "ingestion-bot-jwt");
|
||||
assertEquals(jwt.getClaims().get("isBot").asBoolean(), true);
|
||||
TestUtils.put(getResource(String.format("users/revokeToken/%s", user.getId())), User.class, OK, ADMIN_AUTH_HEADERS);
|
||||
jwtAuthMechanism =
|
||||
TestUtils.get(
|
||||
getResource(String.format("users/token/%s", user.getId())), JWTAuthMechanism.class, ADMIN_AUTH_HEADERS);
|
||||
assertEquals(jwtAuthMechanism.getJWTToken(), StringUtils.EMPTY);
|
||||
}
|
||||
|
||||
private DecodedJWT decodedJWT(String token) throws MalformedURLException, JwkException, HttpResponseException {
|
||||
WebTarget target = getConfigResource("jwks");
|
||||
JWKSResponse auth = TestUtils.get(target, JWKSResponse.class, TEST_AUTH_HEADERS);
|
||||
DecodedJWT jwt;
|
||||
try {
|
||||
jwt = JWT.decode(token);
|
||||
} catch (JWTDecodeException e) {
|
||||
throw new AuthenticationException("Invalid token", e);
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
// if the expiresAt set to null, treat it as never expiring token
|
||||
if (jwt.getExpiresAt() != null
|
||||
&& jwt.getExpiresAt().before(Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTime())) {
|
||||
throw new AuthenticationException("Expired token!");
|
||||
}
|
||||
|
||||
return jwt;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private User createUserAndCheckRoles(
|
||||
CreateUser create, List<EntityReference> expectedRoles, List<EntityReference> expectedInheritedRoles) {
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
package org.openmetadata.catalog.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.auth0.jwt.interfaces.JWTVerifier;
|
||||
import io.dropwizard.testing.ResourceHelpers;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Date;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.openmetadata.catalog.entity.teams.User;
|
||||
import org.openmetadata.catalog.security.jwt.JWTTokenConfiguration;
|
||||
import org.openmetadata.catalog.security.jwt.JWTTokenGenerator;
|
||||
import org.openmetadata.catalog.teams.authn.JWTTokenExpiry;
|
||||
|
||||
@Slf4j
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class JWTTokenGeneratorTest {
|
||||
protected static final String rsaPrivateKeyPath = ResourceHelpers.resourceFilePath("private_key.der");
|
||||
protected static final String rsaPublicKeyPath = ResourceHelpers.resourceFilePath("public_key.der");
|
||||
protected JWTTokenConfiguration jwtTokenConfiguration;
|
||||
protected JWTTokenGenerator jwtTokenGenerator;
|
||||
|
||||
@BeforeAll
|
||||
public void setup(TestInfo test) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
jwtTokenConfiguration = new JWTTokenConfiguration();
|
||||
jwtTokenConfiguration.setJWTIssuer("open-metadata.org");
|
||||
jwtTokenConfiguration.setRSAPrivateKeyFilePath(rsaPrivateKeyPath);
|
||||
jwtTokenConfiguration.setRSAPublicKeyFilePath(rsaPublicKeyPath);
|
||||
jwtTokenGenerator = JWTTokenGenerator.getInstance();
|
||||
jwtTokenGenerator.init(jwtTokenConfiguration);
|
||||
;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGenerateJWTToken() throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
|
||||
User user =
|
||||
new User()
|
||||
.withEmail("ingestion-bot@open-metadata.org")
|
||||
.withName("ingestion-bot")
|
||||
.withDisplayName("ingestion-bot");
|
||||
String token = jwtTokenGenerator.generateJWTToken(user, JWTTokenExpiry.Seven);
|
||||
DecodedJWT jwt = decodedJWT(token);
|
||||
assertEquals(jwt.getClaims().get("sub").asString(), "ingestion-bot");
|
||||
Date date = jwt.getExpiresAt();
|
||||
long daysBetween = ((date.getTime() - jwt.getIssuedAt().getTime()) / (1000 * 60 * 60 * 24));
|
||||
assertTrue(daysBetween >= 6);
|
||||
token = jwtTokenGenerator.generateJWTToken(user, JWTTokenExpiry.Ninety);
|
||||
jwt = decodedJWT(token);
|
||||
date = jwt.getExpiresAt();
|
||||
daysBetween = ((date.getTime() - jwt.getIssuedAt().getTime()) / (1000 * 60 * 60 * 24));
|
||||
assertTrue(daysBetween >= 89);
|
||||
token = jwtTokenGenerator.generateJWTToken(user, JWTTokenExpiry.Unlimited);
|
||||
jwt = decodedJWT(token);
|
||||
assertNull(jwt.getExpiresAt());
|
||||
}
|
||||
|
||||
private DecodedJWT decodedJWT(String token) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
RSAPublicKey publicKey = jwtTokenGenerator.getPublicKey();
|
||||
Algorithm algorithm = Algorithm.RSA256(publicKey, null);
|
||||
JWTVerifier verifier = JWT.require(algorithm).withIssuer(jwtTokenConfiguration.getJWTIssuer()).build();
|
||||
return verifier.verify(token);
|
||||
}
|
||||
}
|
||||
@ -138,6 +138,11 @@ authenticationConfiguration:
|
||||
clientId: "261867039324-neb92r2147i6upchb78tv29idk079bps.apps.googleusercontent.com"
|
||||
callbackUrl: "http://localhost:8585/callback"
|
||||
|
||||
jwtTokenConfiguration:
|
||||
rsapublicKeyFilePath: "src/test/resources/public_key.der"
|
||||
rsaprivateKeyFilePath: "src/test/resources/private_key.der"
|
||||
jwtissuer: "open-metadata.org"
|
||||
keyId: "Gb389a-9f76-gdjs-a92j-0242bk94356"
|
||||
|
||||
eventHandlerConfiguration:
|
||||
eventHandlerClassNames:
|
||||
|
||||
BIN
catalog-rest-service/src/test/resources/private_key.der
Normal file
BIN
catalog-rest-service/src/test/resources/private_key.der
Normal file
Binary file not shown.
@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
gm8VGZIFLJfzi/RqgbdbbypL9HfdDFA=
|
||||
-----END PRIVATE KEY-----
|
||||
BIN
catalog-rest-service/src/test/resources/public_key.der
Normal file
BIN
catalog-rest-service/src/test/resources/public_key.der
Normal file
Binary file not shown.
@ -125,6 +125,7 @@ database:
|
||||
migrationConfiguration:
|
||||
path: "./bootstrap/sql"
|
||||
|
||||
# Authorizer Configuration
|
||||
# Authorizer Configuration
|
||||
authorizerConfiguration:
|
||||
className: ${AUTHORIZER_CLASS_NAME:-org.openmetadata.catalog.security.NoopAuthorizer}
|
||||
@ -142,6 +143,12 @@ authenticationConfiguration:
|
||||
clientId: ${AUTHENTICATION_CLIENT_ID:-""}
|
||||
callbackUrl: ${AUTHENTICATION_CALLBACK_URL:-""}
|
||||
|
||||
jwtTokenConfiguration:
|
||||
rsapublicKeyFilePath: ${RSA_PUBLIC_KEY_FILE_PATH:-""}
|
||||
rsaprivateKeyFilePath: ${RSA_PRIVATE_KEY_FILE_PATH:-""}
|
||||
jwtissuer: ${JWT_ISSUER:-"open-metadata.org"}
|
||||
keyId: ${JWT_KEY_ID:-"Gb389a-9f76-gdjs-a92j-0242bk94356"}
|
||||
|
||||
elasticsearch:
|
||||
host: ${ELASTICSEARCH_HOST:-localhost}
|
||||
port: ${ELASTICSEARCH_PORT:-9200}
|
||||
|
||||
@ -7,5 +7,5 @@ Provides metadata version information.
|
||||
|
||||
from incremental import Version
|
||||
|
||||
__version__ = Version("metadata", 0, 11, 0, dev=0)
|
||||
__version__ = Version("metadata", 0, 11, 0, dev=1)
|
||||
__all__ = ["__version__"]
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
"workflowConfig": {
|
||||
"openMetadataServerConfig": {
|
||||
"hostPort": "http://localhost:8585/api",
|
||||
"authProvider": "no-auth"
|
||||
"authProvider": "no-auth",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
OpenMetadata Airflow Lineage Backend security providers config
|
||||
"""
|
||||
|
||||
|
||||
from airflow.configuration import conf
|
||||
|
||||
from airflow_provider_openmetadata.lineage.config.commons import LINEAGE
|
||||
@ -32,6 +31,9 @@ from metadata.generated.schema.security.client.googleSSOClientConfig import (
|
||||
from metadata.generated.schema.security.client.oktaSSOClientConfig import (
|
||||
OktaSSOClientConfig,
|
||||
)
|
||||
from metadata.generated.schema.security.client.openMetadataJWTClientConfig import (
|
||||
OpenMetadataJWTClientConfig,
|
||||
)
|
||||
from metadata.utils.dispatch import enum_register
|
||||
|
||||
provider_config_registry = enum_register()
|
||||
@ -94,3 +96,11 @@ def load_azure_auth() -> AzureSSOClientConfig:
|
||||
clientId=conf.get(LINEAGE, "client_id"),
|
||||
scopes=conf.get(LINEAGE, "scopes", fallback=[]),
|
||||
)
|
||||
|
||||
|
||||
@provider_config_registry.add(AuthProvider.openmetadata.value)
|
||||
def load_om_auth() -> OpenMetadataJWTClientConfig:
|
||||
"""
|
||||
Load config for Azure Auth
|
||||
"""
|
||||
return OpenMetadataJWTClientConfig(jwtToken=conf.get(LINEAGE, "jwt_token"))
|
||||
|
||||
@ -13,13 +13,16 @@ Interface definition for an Auth provider
|
||||
"""
|
||||
import http.client
|
||||
import json
|
||||
import os.path
|
||||
import sys
|
||||
import traceback
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Tuple
|
||||
|
||||
import requests
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from metadata.config.common import ConfigModel
|
||||
from metadata.generated.schema.entity.services.connections.metadata.openMetadataConnection import (
|
||||
@ -37,6 +40,9 @@ from metadata.generated.schema.security.client.googleSSOClientConfig import (
|
||||
from metadata.generated.schema.security.client.oktaSSOClientConfig import (
|
||||
OktaSSOClientConfig,
|
||||
)
|
||||
from metadata.generated.schema.security.client.openMetadataJWTClientConfig import (
|
||||
OpenMetadataJWTClientConfig,
|
||||
)
|
||||
from metadata.ingestion.ometa.client import APIError
|
||||
from metadata.ingestion.ometa.utils import ometa_logger
|
||||
|
||||
@ -392,3 +398,37 @@ class CustomOIDCAuthenticationProvider(AuthenticationProvider):
|
||||
def get_access_token(self) -> Tuple[str, int]:
|
||||
self.auth_token()
|
||||
return self.generated_auth_token, self.expiry
|
||||
|
||||
|
||||
class OpenMetadataAuthenticationProvider(AuthenticationProvider):
|
||||
"""
|
||||
OpenMetadata authentication implementation
|
||||
|
||||
Args:
|
||||
config (MetadataServerConfig):
|
||||
|
||||
Attributes:
|
||||
config (MetadataServerConfig)
|
||||
"""
|
||||
|
||||
def __init__(self, config: OpenMetadataConnection):
|
||||
self.config = config
|
||||
self.security_config: OpenMetadataJWTClientConfig = self.config.securityConfig
|
||||
self.jwt_token = None
|
||||
self.expiry = datetime.now() - relativedelta(years=1)
|
||||
|
||||
@classmethod
|
||||
def create(cls, config: OpenMetadataConnection):
|
||||
return cls(config)
|
||||
|
||||
def auth_token(self) -> None:
|
||||
if not self.jwt_token:
|
||||
if os.path.isfile(self.security_config.jwtToken):
|
||||
with open(self.security_config.jwtToken, "r") as file:
|
||||
self.jwt_token = file.read().rstrip()
|
||||
else:
|
||||
self.jwt_token = self.security_config.jwtToken
|
||||
|
||||
def get_access_token(self):
|
||||
self.auth_token()
|
||||
return self.jwt_token, self.expiry
|
||||
|
||||
@ -23,6 +23,7 @@ from metadata.ingestion.ometa.auth_provider import (
|
||||
GoogleAuthenticationProvider,
|
||||
NoOpAuthenticationProvider,
|
||||
OktaAuthenticationProvider,
|
||||
OpenMetadataAuthenticationProvider,
|
||||
)
|
||||
from metadata.utils.dispatch import enum_register
|
||||
|
||||
@ -65,3 +66,8 @@ def azure_auth_init(config: OpenMetadataConnection) -> AuthenticationProvider:
|
||||
@auth_provider_registry.add(AuthProvider.custom_oidc.value)
|
||||
def custom_oidc_auth_init(config: OpenMetadataConnection) -> AuthenticationProvider:
|
||||
return CustomOIDCAuthenticationProvider.create(config)
|
||||
|
||||
|
||||
@auth_provider_registry.add(AuthProvider.openmetadata.value)
|
||||
def om_auth_init(config: OpenMetadataConnection) -> AuthenticationProvider:
|
||||
return OpenMetadataAuthenticationProvider.create(config)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user