This is an automated email from the ASF dual-hosted git repository. jfeinauer pushed a commit to branch feature/IOTDB-700-add-openid in repository https://gitbox.apache.org/repos/asf/incubator-iotdb.git
commit c2e146e76583caa298665d47fb9fe84836b3436f Author: julian <[email protected]> AuthorDate: Sun May 24 11:54:47 2020 +0200 IOTBD-700 Initial test Implementation of OpenID Connect / JWT. --- server/pom.xml | 20 ++ .../iotdb/db/auth/authorizer/BasicAuthorizer.java | 13 +- .../db/auth/authorizer/LocalFileAuthorizer.java | 6 + .../iotdb/db/auth/authorizer/OpenIdAuthorizer.java | 234 +++++++++++++++++++++ .../java/org/apache/iotdb/db/conf/IoTDBConfig.java | 11 + .../org/apache/iotdb/db/service/TSServiceImpl.java | 3 +- .../db/auth/authorizer/OpenIdAuthorizerTest.java | 60 ++++++ 7 files changed, 341 insertions(+), 6 deletions(-) diff --git a/server/pom.xml b/server/pom.xml index b6cb127..924799d 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -145,6 +145,26 @@ <artifactId>stream</artifactId> <version>2.9.5</version> </dependency> + <!-- compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'--> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-api</artifactId> + <version>0.10.7</version> + </dependency> + <!-- Impl --> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-impl</artifactId> + <version>0.10.7</version> + <scope>runtime</scope> + </dependency> + <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson --> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-jackson</artifactId> + <version>0.10.7</version> + <scope>runtime</scope> + </dependency> </dependencies> <build> <plugins> diff --git a/server/src/main/java/org/apache/iotdb/db/auth/authorizer/BasicAuthorizer.java b/server/src/main/java/org/apache/iotdb/db/auth/authorizer/BasicAuthorizer.java index 3659d2d..6bbeb1c 100644 --- a/server/src/main/java/org/apache/iotdb/db/auth/authorizer/BasicAuthorizer.java +++ b/server/src/main/java/org/apache/iotdb/db/auth/authorizer/BasicAuthorizer.java @@ -63,6 +63,9 @@ public abstract class BasicAuthorizer implements IAuthorizer, IService { logger.info("Initialization of Authorizer completes"); } + /** Checks if a user has admin privileges */ + abstract boolean isAdmin(String username); + @Override public boolean login(String username, String password) throws AuthException { User user = userManager.getUser(username); @@ -78,7 +81,7 @@ public abstract class BasicAuthorizer implements IAuthorizer, IService { @Override public void deleteUser(String username) throws AuthException { - if (IoTDBConstant.ADMIN_NAME.equals(username)) { + if (isAdmin(username)) { throw new AuthException("Default administrator cannot be deleted"); } if (!userManager.deleteUser(username)) { @@ -90,7 +93,7 @@ public abstract class BasicAuthorizer implements IAuthorizer, IService { public void grantPrivilegeToUser(String username, String path, int privilegeId) throws AuthException { String newPath = path; - if (IoTDBConstant.ADMIN_NAME.equals(username)) { + if (!isAdmin(username)) { throw new AuthException("Invalid operation, administrator already has all privileges"); } if (!PrivilegeType.isPathRelevant(privilegeId)) { @@ -105,7 +108,7 @@ public abstract class BasicAuthorizer implements IAuthorizer, IService { @Override public void revokePrivilegeFromUser(String username, String path, int privilegeId) throws AuthException { - if (IoTDBConstant.ADMIN_NAME.equals(username)) { + if (isAdmin(username)) { throw new AuthException("Invalid operation, administrator must have all privileges"); } String p = path; @@ -204,7 +207,7 @@ public abstract class BasicAuthorizer implements IAuthorizer, IService { @Override public Set<Integer> getPrivileges(String username, String path) throws AuthException { - if (IoTDBConstant.ADMIN_NAME.equals(username)) { + if (isAdmin(username)) { return ADMIN_PRIVILEGES; } User user = userManager.getUser(username); @@ -233,7 +236,7 @@ public abstract class BasicAuthorizer implements IAuthorizer, IService { @Override public boolean checkUserPrivileges(String username, String path, int privilegeId) throws AuthException { - if (IoTDBConstant.ADMIN_NAME.equals(username)) { + if (isAdmin(username)) { return true; } User user = userManager.getUser(username); diff --git a/server/src/main/java/org/apache/iotdb/db/auth/authorizer/LocalFileAuthorizer.java b/server/src/main/java/org/apache/iotdb/db/auth/authorizer/LocalFileAuthorizer.java index c2d94ce..2a76648 100644 --- a/server/src/main/java/org/apache/iotdb/db/auth/authorizer/LocalFileAuthorizer.java +++ b/server/src/main/java/org/apache/iotdb/db/auth/authorizer/LocalFileAuthorizer.java @@ -23,6 +23,7 @@ import org.apache.iotdb.db.auth.AuthException; import org.apache.iotdb.db.auth.role.LocalFileRoleManager; import org.apache.iotdb.db.auth.user.LocalFileUserManager; import org.apache.iotdb.db.conf.IoTDBConfig; +import org.apache.iotdb.db.conf.IoTDBConstant; import org.apache.iotdb.db.conf.IoTDBDescriptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +38,11 @@ public class LocalFileAuthorizer extends BasicAuthorizer { new LocalFileRoleManager(config.getSystemDir() + File.separator + "roles")); } + @Override + boolean isAdmin(String username) { + return IoTDBConstant.ADMIN_NAME.equals(username); + } + /** * function for getting the instance of the local file authorizer. */ diff --git a/server/src/main/java/org/apache/iotdb/db/auth/authorizer/OpenIdAuthorizer.java b/server/src/main/java/org/apache/iotdb/db/auth/authorizer/OpenIdAuthorizer.java new file mode 100644 index 0000000..5330000 --- /dev/null +++ b/server/src/main/java/org/apache/iotdb/db/auth/authorizer/OpenIdAuthorizer.java @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or ag [...] + */ + +package org.apache.iotdb.db.auth.authorizer; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import org.apache.iotdb.db.auth.AuthException; +import org.apache.iotdb.db.auth.role.IRoleManager; +import org.apache.iotdb.db.auth.role.LocalFileRoleManager; +import org.apache.iotdb.db.auth.user.IUserManager; +import org.apache.iotdb.db.auth.user.LocalFileUserManager; +import org.apache.iotdb.db.conf.IoTDBConfig; +import org.apache.iotdb.db.conf.IoTDBDescriptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; + +/** + * Uses an OpenID Connect provider for Authorization / Authentication. + */ +public class OpenIdAuthorizer extends BasicAuthorizer { + + private static final Logger logger = LoggerFactory.getLogger(OpenIdAuthorizer.class); + + private static IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig(); + + private final String secret; + + public OpenIdAuthorizer() throws AuthException { + this(config.getOpenIdSecret()); + } + + OpenIdAuthorizer(String secret) throws AuthException { + super(new LocalFileUserManager(config.getSystemDir() + File.separator + "users"), + new LocalFileRoleManager(config.getSystemDir() + File.separator + "roles")); + if (secret == null) { + throw new IllegalArgumentException("OpenID Secret is null which is not allowed!"); + } + this.secret = secret; + } + + /** + * function for getting the instance of the local file authorizer. + */ + public static OpenIdAuthorizer getInstance() throws AuthException { + if (OpenIdAuthorizer.InstanceHolder.instance == null) { + throw new AuthException("Authorizer uninitialized"); + } + return OpenIdAuthorizer.InstanceHolder.instance; + } + + private static class InstanceHolder { + private static OpenIdAuthorizer instance; + + static { + // Only for testing here! + IoTDBDescriptor.getInstance().getConfig().setOpenIdSecret("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"); + try { + instance = new OpenIdAuthorizer(); + } catch (AuthException e) { + logger.error("Authorizer initialization failed due to ", e); + instance = null; + } + } + } + + @Override + public boolean login(String token, String password) throws AuthException { + if (password != null && !password.isEmpty()) { + logger.error("JWT Login failed as a non-empty Password was given username (token): {}, password: {}", token, password); + return false; + } + if (token == null || token.isEmpty()) { + logger.error("JWT Login failed as a Username (token) was empty!"); + return false; + } + //This line will throw an exception if it is not a signed JWS (as expected) + Claims claims; + try { + claims = validateToken(token); + } catch (JwtException e) { + logger.error("Unable to login the user wit jwt {}", password, e); + return false; + } + logger.debug("JWT was validated successfully!"); + logger.debug("ID: {}", claims.getId()); + logger.debug("Subject: {}", claims.getSubject()); + logger.debug("Issuer: {}", claims.getIssuer()); + logger.debug("Expiration: {}", claims.getExpiration()); + // Create User if not exists + if (!super.listAllUsers().contains(claims.getId())) { + logger.info("User {} logs in for first time, storing it locally!", claims.getId()); + super.createUser(claims.getSubject(), "UNUSED_PASSWORT"); + } + return true; + } + + private Claims validateToken(String token) throws JwtException { + return Jwts + .parser() + // Basically ignore the Expiration Date, if there is any??? + .setAllowedClockSkewSeconds(Long.MAX_VALUE / 1000) + // .setSigningKey(DatatypeConverter.parseBase64Binary(secret)) + .setSigningKey(secret.getBytes()) + .parseClaimsJws(token) + .getBody(); + } + + @Override + public void createUser(String username, String password) throws AuthException { + throw new UnsupportedOperationException("This operation is not supported for JWT Auth Provider!"); + } + + @Override + public void deleteUser(String username) throws AuthException { + throw new UnsupportedOperationException("This operation is not supported for JWT Auth Provider!"); + } + + @Override + boolean isAdmin(String token) { + Claims claims; + try { + claims = validateToken(token); + } catch (JwtException e) { + logger.warn("Unable to validate token {}!", token, e); + return false; + } + if (!(claims.get("IOTDB_ADMIN") instanceof Boolean) || !claims.get("IOTDB_ADMIN", Boolean.class)) { + logger.warn("Given Token has no admin rights, is custom claim IOTDB_ADMIN set to true?"); + return false; + } + return true; + } + +// @Override +// public void grantPrivilegeToUser(String username, String path, int privilegeId) throws AuthException { +// if (isAdmin(username)) { +// throw new AuthException("Given Token has no Admin privileges!"); +// } +// // Yes, you are Admin! Gratz! +// // Do something here... +// super.grantPrivilegeToUser(username, path, privilegeId); +// } +// @Override +// public void revokePrivilegeFromUser(String username, String path, int privilegeId) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public void createRole(String roleName) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public void deleteRole(String roleName) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public void grantPrivilegeToRole(String roleName, String path, int privilegeId) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public void revokePrivilegeFromRole(String roleName, String path, int privilegeId) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public void grantRoleToUser(String roleName, String username) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public void revokeRoleFromUser(String roleName, String username) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public Set<Integer> getPrivileges(String username, String path) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } + + @Override + public void updateUserPassword(String username, String newPassword) throws AuthException { + throw new UnsupportedOperationException("This operation is not supported for JWT Auth Provider!"); + } +// +// @Override +// public boolean checkUserPrivileges(String username, String path, int privilegeId) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public void reset() throws AuthException { +// // Do nothing +// super.reset(); +// } +// +// @Override +// public List<String> listAllUsers() { +// // Unsure if we list all "known" users or just throw this exception?? +// throw new UnsupportedOperationException("This operation is not supported for JWT Auth Provider!"); +// } +// +// @Override +// public List<String> listAllRoles() { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public Role getRole(String roleName) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public User getUser(String username) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public boolean isUserUseWaterMark(String userName) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +// +// @Override +// public void setUserUseWaterMark(String userName, boolean useWaterMark) throws AuthException { +// throw new NotImplementedException("Not yet implemented!"); +// } +} diff --git a/server/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java b/server/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java index 0d04592..ad3fabc 100644 --- a/server/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java +++ b/server/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java @@ -548,6 +548,9 @@ public class IoTDBConfig { // max size for tag and attribute of one time series private int tagAttributeTotalSize = 700; + // Open ID Secret + private String openIdSecret = null; + public IoTDBConfig() { // empty constructor } @@ -1506,4 +1509,12 @@ public class IoTDBConfig { public void setPrimitiveArraySize(int primitiveArraySize) { this.primitiveArraySize = primitiveArraySize; } + + public String getOpenIdSecret() { + return openIdSecret; + } + + public void setOpenIdSecret(String openIdSecret) { + this.openIdSecret = openIdSecret; + } } diff --git a/server/src/main/java/org/apache/iotdb/db/service/TSServiceImpl.java b/server/src/main/java/org/apache/iotdb/db/service/TSServiceImpl.java index 3beb9c9..052e27c 100644 --- a/server/src/main/java/org/apache/iotdb/db/service/TSServiceImpl.java +++ b/server/src/main/java/org/apache/iotdb/db/service/TSServiceImpl.java @@ -41,6 +41,7 @@ import org.apache.iotdb.db.auth.AuthException; import org.apache.iotdb.db.auth.AuthorityChecker; import org.apache.iotdb.db.auth.authorizer.IAuthorizer; import org.apache.iotdb.db.auth.authorizer.LocalFileAuthorizer; +import org.apache.iotdb.db.auth.authorizer.OpenIdAuthorizer; import org.apache.iotdb.db.conf.IoTDBConfig; import org.apache.iotdb.db.conf.IoTDBConstant; import org.apache.iotdb.db.conf.IoTDBDescriptor; @@ -180,7 +181,7 @@ public class TSServiceImpl implements TSIService.Iface, ServerContext { boolean status; IAuthorizer authorizer; try { - authorizer = LocalFileAuthorizer.getInstance(); + authorizer = OpenIdAuthorizer.getInstance(); } catch (AuthException e) { throw new TException(e); } diff --git a/server/src/test/java/org/apache/iotdb/db/auth/authorizer/OpenIdAuthorizerTest.java b/server/src/test/java/org/apache/iotdb/db/auth/authorizer/OpenIdAuthorizerTest.java new file mode 100644 index 0000000..6470656 --- /dev/null +++ b/server/src/test/java/org/apache/iotdb/db/auth/authorizer/OpenIdAuthorizerTest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or ag [...] + */ + +package org.apache.iotdb.db.auth.authorizer; + +import org.apache.iotdb.db.auth.AuthException; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class OpenIdAuthorizerTest { + + @Test + public void loginWithJWT() throws AuthException { + String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoiMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.PB603vtDyNkryxeLjomX1JQuSF2JHKXHyixzPBCA7tQ"; + String secret = "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"; + + OpenIdAuthorizer authorizer = new OpenIdAuthorizer(secret); + boolean login = authorizer.login(jwt, null); + + assertTrue(login); + } + + @Test + public void isAdmin_hasAccess() throws AuthException { + // IOTDB_ADMIN = true + String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoiMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJJT1REQl9BRE1JTiI6dHJ1ZX0.dxB417n9GFAGbwL7kyIvgenEBycjlJLZbB1I_GF0qd8"; + String secret = "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"; + + OpenIdAuthorizer authorizer = new OpenIdAuthorizer(secret); + boolean admin = authorizer.isAdmin(jwt); + + assertTrue(admin); + } + + @Test + public void isAdmin_AdminClaimFalse() throws AuthException { + // IOTDB_ADMIN = false + String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoiMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJJT1REQl9BRE1JTiI6ZmFsc2V9.80lCGEWhgW6YO55TFC98v_mj8ts0IcrBMb2drsxEpZ0"; + String secret = "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"; + + OpenIdAuthorizer authorizer = new OpenIdAuthorizer(secret); + boolean admin = authorizer.isAdmin(jwt); + + assertFalse(admin); + } + + @Test + public void isAdmin_noAdminClaim() throws AuthException { + // IOTDB_ADMIN = false + String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoiMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.PB603vtDyNkryxeLjomX1JQuSF2JHKXHyixzPBCA7tQ"; + String secret = "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"; + + OpenIdAuthorizer authorizer = new OpenIdAuthorizer(secret); + boolean admin = authorizer.isAdmin(jwt); + + assertFalse(admin); + } +} \ No newline at end of file
