Fix #4637: Support JWT Token generation for bot accounts (#4647)

* 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:
Sriharsha Chintalapani 2022-05-05 03:02:33 -07:00 committed by GitHub
parent 1c68748618
commit 4531190af6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 784 additions and 11 deletions

View File

@ -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()));

View File

@ -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

View File

@ -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;
}

View File

@ -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());
}
}
}
}
}

View File

@ -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();
}
}

View File

@ -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)

View File

@ -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");
}
}

View File

@ -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";
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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:

View File

@ -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"
}
]
},

View File

@ -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"]
}

View File

@ -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
}

View File

@ -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"

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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"]
}

View File

@ -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());
}
}

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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:

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
gm8VGZIFLJfzi/RqgbdbbypL9HfdDFA=
-----END PRIVATE KEY-----

Binary file not shown.

View File

@ -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}

View File

@ -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__"]

View File

@ -17,7 +17,7 @@
"workflowConfig": {
"openMetadataServerConfig": {
"hostPort": "http://localhost:8585/api",
"authProvider": "no-auth"
"authProvider": "no-auth",
}
}
}

View File

@ -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"))

View File

@ -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

View File

@ -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)