diff --git a/iotdb-core/datanode/pom.xml b/iotdb-core/datanode/pom.xml index 8b5302048964f..e894851fc11ca 100644 --- a/iotdb-core/datanode/pom.xml +++ b/iotdb-core/datanode/pom.xml @@ -277,33 +277,11 @@ mockito-core test - - - io.jsonwebtoken - jjwt-impl - test - - - - io.jsonwebtoken - jjwt-jackson - test - - - net.minidev - json-smart - test - org.apache.ratis ratis-thirdparty-misc runtime - - com.nimbusds - oauth2-oidc-sdk - test - org.powermock powermock-core @@ -488,11 +466,6 @@ org.apache.iotdb:isession - - - io.jsonwebtoken:jjwt-impl - io.jsonwebtoken:jjwt-jackson - diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/auth/authorizer/OpenIdAuthorizerTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/auth/authorizer/OpenIdAuthorizerTest.java deleted file mode 100644 index 196cc80e5b6da..0000000000000 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/auth/authorizer/OpenIdAuthorizerTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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 agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.iotdb.db.auth.authorizer; - -import org.apache.iotdb.commons.auth.AuthException; -import org.apache.iotdb.commons.auth.authorizer.OpenIdAuthorizer; -import org.apache.iotdb.commons.conf.CommonConfig; -import org.apache.iotdb.commons.conf.CommonDescriptor; -import org.apache.iotdb.db.utils.EnvironmentUtils; - -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -import java.io.IOException; -import java.net.URISyntaxException; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class OpenIdAuthorizerTest { - - private static final String OPEN_ID_PUBLIC_JWK = - "{\"kty\":\"RSA\",\"x5t#S256\":\"TZFbbj6HsRU28HYvrcVnDs03KreV3DE24-Cxb9EPdS4\",\"e\":\"AQAB\",\"use\":\"sig\",\"x5t\":\"l_N2UlC_a624iu5eYFypnB1Wr20\",\"kid\":\"q1-Wm0ozQ5O0mQH8-SJap2ZcN4MmucWwnQWKYxZJ4ow\",\"x5c\":[\"MIICmTCCAYECBgFyRdXW2DANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVJb1REQjAeFw0yMDA1MjQwODM3MjJaFw0zMDA1MjQwODM5MDJaMBAxDjAMBgNVBAMMBUlvVERCMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAozDCZTVc9946VvhZ6E\\/OP8Yx6tJe0i9GR2Q9jR9S3jQoo0haT\\/P1b\\/zvQK52qA1xj6tBVg64xl3+LUxtCvh3HfAM5Q3PeSa0e2MkZaKCt335lKnKCSuaQGYoHULmg\\/FDOgCA0wJYOonGGJkgWmkzSAzdnHmBATosTl0XkBXHTdFOq5HaKw+bfghYp5097Gkl\\/Dp4sixVjIWLTh5l9diy4D\\/XKxadGumPCmTOS5E7y92jiHE64XFe1Q7v1qD+qKJKFvamAMIFPGBKegIajt42IcOIcIaJZnM1lBZApq1a\\/E6oL24QnP\\/j2e9coseDtGNywaADQdO8PaJadH\\/BV4aPCwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBX4rsWPIAwgSK6BEZmtEkh\\/FMfZtkvCFANpwkCX5Pph8yuk\\/8xrvx30yb4fIgqsxxQk6H+Q1qptm1cXs0tNu1yft+t+B2VuVjrWtkCkV0hAy6eZcdW411Pt523pHoOTxg6ehQd5DsvCIlsvWo83ePTKME+092vfs3irfQcRzc5xINdpopSvZlZuQ83tNEJY8gWvspQZr+uj8AP2x6w0BOrPJIiLlV+peNJuD3UgJKlSfOueKbKeM1kIVOG\\/a2AoEkBgqktnaIWzkXbk475\\/0xfGegsSZrxGR3\\/SA3jegS0sHFCY7\\/Ie\\/UvDgqMjd207oT64jxEGrd4mObxOx7aS0tp\"],\"alg\":\"RS256\",\"n\":\"ozDCZTVc9946VvhZ6E_OP8Yx6tJe0i9GR2Q9jR9S3jQoo0haT_P1b_zvQK52qA1xj6tBVg64xl3-LUxtCvh3HfAM5Q3PeSa0e2MkZaKCt335lKnKCSuaQGYoHULmg_FDOgCA0wJYOonGGJkgWmkzSAzdnHmBATosTl0XkBXHTdFOq5HaKw-bfghYp5097Gkl_Dp4sixVjIWLTh5l9diy4D_XKxadGumPCmTOS5E7y92jiHE64XFe1Q7v1qD-qKJKFvamAMIFPGBKegIajt42IcOIcIaJZnM1lBZApq1a_E6oL24QnP_j2e9coseDtGNywaADQdO8PaJadH_BV4aPCw\"}"; - private static CommonConfig config; - - @Before - public void setUp() throws Exception { - EnvironmentUtils.envSetUp(); - config = CommonDescriptor.getInstance().getConfig(); - } - - @After - public void tearDown() throws Exception { - EnvironmentUtils.cleanEnv(); - } - - @Test - public void loginWithJWT() throws AuthException, ParseException { - String jwt = - "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJxMS1XbTBvelE1TzBtUUg4LVNKYXAyWmNONE1tdWNXd25RV0tZeFpKNG93In0.eyJleHAiOjE1OTAzMTcxNzYsImlhdCI6MTU5MDMxNjg3NiwianRpIjoiY2MyNWQ3MDAtYjc5NC00OTA4LTg0OGUtOTRhNzYzNmM5YzQxIiwiaXNzIjoiaHR0cDovL2F1dGguZGVtby5wcmFnbWF0aWNpbmR1c3RyaWVzLmRlL2F1dGgvcmVhbG1zL0lvVERCIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6Ijg2YWRmNGIzLWE4ZTUtNDc1NC1iNWEwLTQ4OGI0OWY0M2VkMiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImlvdGRiIiwic2Vzc2lvbl9zdGF0ZSI6Ijk0ZmI5NGZjLTg3YTMtNDg4Ny04M2Q3LWE5MmQ1MzMzOTMzMCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImNsaWVudEhvc3QiOiIxOTIuMTY4LjE2OS4yMSIsImNsaWVudElkIjoiaW90ZGIiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1pb3RkYiIsImNsaWVudEFkZHJlc3MiOiIxOTIuMTY4LjE2OS4yMSJ9.GxQFltm1PrZzVL7rR6K-GpQINFLymjqAxxoDt_DGfQEMt61M6ebmx2oHiP_3G0HDSl7sbamajQbbRrfyTg--emBC2wfhdZ7v_7O0qWC60Yd8cWZ9qxwqwTFKYb8a0Z6_TeH9-vUmsy6kp2BfJZXq3mSy0My21VGUAXRmWTbghiM4RFoHKjAZVhsPHWelFmtLftYPdOGxv-7c9iUOVh_W-nOcCNRJpYY7BEjUYN24TsjvCEwWDQWD9E29LMYfA6LNeG0KdL9Jvqad4bc2FTJn9TaCnJMCiAJ7wEEiotqhXn70uEBWYxGXIVlm3vn3MDe3pTKA2TZy7U5xcrE7S8aGMg"; - - OpenIdAuthorizer authorizer = new OpenIdAuthorizer(JSONObjectUtils.parse(OPEN_ID_PUBLIC_JWK)); - boolean login = authorizer.login(jwt, null, false); - - assertTrue(login); - } - - @Test - public void isAdmin_hasAccess() throws AuthException, ParseException { - // IOTDB_ADMIN = true - String jwt = - "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJxMS1XbTBvelE1TzBtUUg4LVNKYXAyWmNONE1tdWNXd25RV0tZeFpKNG93In0.eyJleHAiOjE1OTAzMjM5MjgsImlhdCI6MTU5MDMyMzYyOCwianRpIjoiZGQ5ZDZhNmItZjgzOC00Mjk3LTg5YWUtMjdlZTgxNzVhMThiIiwiaXNzIjoiaHR0cDovL2F1dGguZGVtby5wcmFnbWF0aWNpbmR1c3RyaWVzLmRlL2F1dGgvcmVhbG1zL0lvVERCIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImJhMzJlNDcxLWM3NzItNGIzMy04ZGE2LTZmZThhY2RhMDA3MyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImlvdGRiIiwic2Vzc2lvbl9zdGF0ZSI6IjViZDRhNmM5LTBmYzItNGIxMy05Y2QxLTFhN2NjMzk3NjVhNyIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImlvdGRiX2FkbWluIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyIn0.LthDI93A3jqATc_8Fm0ismqQSN62TUknD6ara6w43eao2hY6KBoMXVY1c6SXSy8hhQeHLiWpopjJE6fsG9xbaV2Gs24SJYnP4DkHvhULlBJ_PUjFy18QxzeexwYK358a99eVHG_8yu-f2kN3mJslOSrlny8oZDxeSxUi9wYNIuQFeLPmGfSISVFn_5V8lpoUAHeENmf9h8mSyEcUHGqtZfVm5zEYIbPPSBqvNei2NvKAFle6qoaJ1l13dpbw39KkOtIUF8dJ7v8XY_xgO2GXCJCvZ5YGr-q4UnA9v_GM3h3vSa5dyCuG0HXBmAujxSxywzPl5RB_QCTiYcTm7MGKLg"; - - OpenIdAuthorizer authorizer = new OpenIdAuthorizer(JSONObjectUtils.parse(OPEN_ID_PUBLIC_JWK)); - boolean admin = authorizer.isAdmin(jwt); - - assertTrue(admin); - } - - @Test - public void isAdmin_noAdminClaim() throws AuthException, ParseException { - // IOTDB_ADMIN = false - String jwt = - "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJxMS1XbTBvelE1TzBtUUg4LVNKYXAyWmNONE1tdWNXd25RV0tZeFpKNG93In0.eyJleHAiOjE1OTAzMTcxNzYsImlhdCI6MTU5MDMxNjg3NiwianRpIjoiY2MyNWQ3MDAtYjc5NC00OTA4LTg0OGUtOTRhNzYzNmM5YzQxIiwiaXNzIjoiaHR0cDovL2F1dGguZGVtby5wcmFnbWF0aWNpbmR1c3RyaWVzLmRlL2F1dGgvcmVhbG1zL0lvVERCIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6Ijg2YWRmNGIzLWE4ZTUtNDc1NC1iNWEwLTQ4OGI0OWY0M2VkMiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImlvdGRiIiwic2Vzc2lvbl9zdGF0ZSI6Ijk0ZmI5NGZjLTg3YTMtNDg4Ny04M2Q3LWE5MmQ1MzMzOTMzMCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImNsaWVudEhvc3QiOiIxOTIuMTY4LjE2OS4yMSIsImNsaWVudElkIjoiaW90ZGIiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1pb3RkYiIsImNsaWVudEFkZHJlc3MiOiIxOTIuMTY4LjE2OS4yMSJ9.GxQFltm1PrZzVL7rR6K-GpQINFLymjqAxxoDt_DGfQEMt61M6ebmx2oHiP_3G0HDSl7sbamajQbbRrfyTg--emBC2wfhdZ7v_7O0qWC60Yd8cWZ9qxwqwTFKYb8a0Z6_TeH9-vUmsy6kp2BfJZXq3mSy0My21VGUAXRmWTbghiM4RFoHKjAZVhsPHWelFmtLftYPdOGxv-7c9iUOVh_W-nOcCNRJpYY7BEjUYN24TsjvCEwWDQWD9E29LMYfA6LNeG0KdL9Jvqad4bc2FTJn9TaCnJMCiAJ7wEEiotqhXn70uEBWYxGXIVlm3vn3MDe3pTKA2TZy7U5xcrE7S8aGMg"; - - OpenIdAuthorizer authorizer = new OpenIdAuthorizer(JSONObjectUtils.parse(OPEN_ID_PUBLIC_JWK)); - boolean admin = authorizer.isAdmin(jwt); - - assertFalse(admin); - } - - /** Can be run manually as long as the site below is active... */ - @Test - @Ignore("We have to find a way to test this against a defined OIDC Provider") - public void fetchMetadata() - throws ParseException, IOException, URISyntaxException, AuthException { - OpenIdAuthorizer openIdAuthorizer = - new OpenIdAuthorizer("https://auth.demo.pragmaticindustries.de/auth/realms/IoTDB/"); - boolean login = - openIdAuthorizer.login( - "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJxMS1XbTBvelE1TzBtUUg4LVNKYXAyWmNONE1tdWNXd25RV0tZeFpKNG93In0.eyJleHAiOjE1OTAzMTcxNzYsImlhdCI6MTU5MDMxNjg3NiwianRpIjoiY2MyNWQ3MDAtYjc5NC00OTA4LTg0OGUtOTRhNzYzNmM5YzQxIiwiaXNzIjoiaHR0cDovL2F1dGguZGVtby5wcmFnbWF0aWNpbmR1c3RyaWVzLmRlL2F1dGgvcmVhbG1zL0lvVERCIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6Ijg2YWRmNGIzLWE4ZTUtNDc1NC1iNWEwLTQ4OGI0OWY0M2VkMiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImlvdGRiIiwic2Vzc2lvbl9zdGF0ZSI6Ijk0ZmI5NGZjLTg3YTMtNDg4Ny04M2Q3LWE5MmQ1MzMzOTMzMCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImNsaWVudEhvc3QiOiIxOTIuMTY4LjE2OS4yMSIsImNsaWVudElkIjoiaW90ZGIiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1pb3RkYiIsImNsaWVudEFkZHJlc3MiOiIxOTIuMTY4LjE2OS4yMSJ9.GxQFltm1PrZzVL7rR6K-GpQINFLymjqAxxoDt_DGfQEMt61M6ebmx2oHiP_3G0HDSl7sbamajQbbRrfyTg--emBC2wfhdZ7v_7O0qWC60Yd8cWZ9qxwqwTFKYb8a0Z6_TeH9-vUmsy6kp2BfJZXq3mSy0My21VGUAXRmWTbghiM4RFoHKjAZVhsPHWelFmtLftYPdOGxv-7c9iUOVh_W-nOcCNRJpYY7BEjUYN24TsjvCEwWDQWD9E29LMYfA6LNeG0KdL9Jvqad4bc2FTJn9TaCnJMCiAJ7wEEiotqhXn70uEBWYxGXIVlm3vn3MDe3pTKA2TZy7U5xcrE7S8aGMg", - "", - false); - assertTrue(login); - config.setOpenIdProviderUrl("https://auth.demo.pragmaticindustries.de/auth/realms/IoTDB/"); - OpenIdAuthorizer openIdAuthorizer1 = new OpenIdAuthorizer(); - login = - openIdAuthorizer1.login( - "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJxMS1XbTBvelE1TzBtUUg4LVNKYXAyWmNONE1tdWNXd25RV0tZeFpKNG93In0.eyJleHAiOjE1OTAzMTcxNzYsImlhdCI6MTU5MDMxNjg3NiwianRpIjoiY2MyNWQ3MDAtYjc5NC00OTA4LTg0OGUtOTRhNzYzNmM5YzQxIiwiaXNzIjoiaHR0cDovL2F1dGguZGVtby5wcmFnbWF0aWNpbmR1c3RyaWVzLmRlL2F1dGgvcmVhbG1zL0lvVERCIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6Ijg2YWRmNGIzLWE4ZTUtNDc1NC1iNWEwLTQ4OGI0OWY0M2VkMiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImlvdGRiIiwic2Vzc2lvbl9zdGF0ZSI6Ijk0ZmI5NGZjLTg3YTMtNDg4Ny04M2Q3LWE5MmQ1MzMzOTMzMCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImNsaWVudEhvc3QiOiIxOTIuMTY4LjE2OS4yMSIsImNsaWVudElkIjoiaW90ZGIiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1pb3RkYiIsImNsaWVudEFkZHJlc3MiOiIxOTIuMTY4LjE2OS4yMSJ9.GxQFltm1PrZzVL7rR6K-GpQINFLymjqAxxoDt_DGfQEMt61M6ebmx2oHiP_3G0HDSl7sbamajQbbRrfyTg--emBC2wfhdZ7v_7O0qWC60Yd8cWZ9qxwqwTFKYb8a0Z6_TeH9-vUmsy6kp2BfJZXq3mSy0My21VGUAXRmWTbghiM4RFoHKjAZVhsPHWelFmtLftYPdOGxv-7c9iUOVh_W-nOcCNRJpYY7BEjUYN24TsjvCEwWDQWD9E29LMYfA6LNeG0KdL9Jvqad4bc2FTJn9TaCnJMCiAJ7wEEiotqhXn70uEBWYxGXIVlm3vn3MDe3pTKA2TZy7U5xcrE7S8aGMg", - "", - false); - assertTrue(login); - } -} diff --git a/iotdb-core/node-commons/pom.xml b/iotdb-core/node-commons/pom.xml index 24f33d0de8bec..7c54c64b32873 100644 --- a/iotdb-core/node-commons/pom.xml +++ b/iotdb-core/node-commons/pom.xml @@ -149,6 +149,16 @@ io.jsonwebtoken jjwt-api + + io.jsonwebtoken + jjwt-impl + test + + + io.jsonwebtoken + jjwt-jackson + test + com.nimbusds oauth2-oidc-sdk @@ -267,6 +277,16 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + io.jsonwebtoken:jjwt-impl + io.jsonwebtoken:jjwt-jackson + + + org.apache.maven.plugins maven-surefire-plugin diff --git a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template index 3f1c4ab41ffaf..5d5cce4123f8c 100644 --- a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template +++ b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template @@ -1753,6 +1753,12 @@ authorizer_provider_class=org.apache.iotdb.commons.auth.authorizer.LocalFileAuth # Privilege: SECURITY openID_url= +# If OpenIdAuthorizer is enabled, then openID_audience must contain the IoTDB client ID +# or a comma-separated allowlist of accepted audiences. +# effectiveMode: restart +# Privilege: SECURITY +openID_audience= + # encryption provider class # effectiveMode: first_start iotdb_server_encrypt_decrypt_provider=org.apache.iotdb.commons.security.encrypt.MessageDigestEncrypt diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizer.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizer.java index ee66ee5bced95..f073984060999 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizer.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizer.java @@ -45,22 +45,30 @@ import java.net.URISyntaxException; import java.net.URL; import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Scanner; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; /** Uses an OpenID Connect provider for Authorization / Authentication. */ public class OpenIdAuthorizer extends BasicAuthorizer { private static final Logger logger = LoggerFactory.getLogger(OpenIdAuthorizer.class); + private static final long MAX_CLOCK_SKEW_SECONDS = 300; public static final String IOTDB_ADMIN_ROLE_NAME = "iotdb_admin"; public static final String OPENID_USER_PREFIX = "openid-"; private static final CommonConfig config = CommonDescriptor.getInstance().getConfig(); private final RSAPublicKey providerKey; + private final String expectedIssuer; + private final Set acceptedAudiences; /** Stores all claims to the respective user */ private final Map loggedClaims = new HashMap<>(); @@ -70,6 +78,11 @@ public OpenIdAuthorizer() throws AuthException, ParseException, IOException, URI } public OpenIdAuthorizer(JSONObject jwk) throws AuthException { + this(jwk, null, Collections.emptySet()); + } + + public OpenIdAuthorizer(JSONObject jwk, String expectedIssuer, Set acceptedAudiences) + throws AuthException { super( new LocalFileUserManager(config.getUserFolder()), new LocalFileRoleManager(config.getRoleFolder())); @@ -79,15 +92,20 @@ public OpenIdAuthorizer(JSONObject jwk) throws AuthException { throw new AuthException( TSStatusCode.INIT_AUTH_ERROR, "Unable to get OIDC Provider Key from JWK " + jwk, e); } - logger.info("Initialized with providerKey: {}", providerKey); + this.expectedIssuer = expectedIssuer; + this.acceptedAudiences = Collections.unmodifiableSet(new HashSet<>(acceptedAudiences)); } public OpenIdAuthorizer(String providerUrl) throws AuthException, URISyntaxException, ParseException, IOException { - this(getJwkFromProvider(providerUrl)); + this(loadProviderContext(providerUrl)); + } + + private OpenIdAuthorizer(ProviderContext providerContext) throws AuthException { + this(providerContext.jwk, providerContext.issuer, providerContext.acceptedAudiences); } - private static JSONObject getJwkFromProvider(String providerUrl) + private static ProviderContext loadProviderContext(String providerUrl) throws URISyntaxException, IOException, ParseException, AuthException { if (providerUrl == null) { throw new IllegalArgumentException("OpenID Connect Provider URI must be given!"); @@ -96,12 +114,23 @@ private static JSONObject getJwkFromProvider(String providerUrl) // Fetch Metadata OIDCProviderMetadata providerMetadata = fetchMetadata(providerUrl); - logger.debug("Using Provider Metadata: {}", providerMetadata); + Set acceptedAudiences = parseAudiences(config.getOpenIdAudience()); + if (acceptedAudiences.isEmpty()) { + throw new AuthException( + TSStatusCode.INIT_AUTH_ERROR, + "openID_audience must be configured when OpenIdAuthorizer is enabled"); + } + + String issuer = + providerMetadata.getIssuer() == null ? null : providerMetadata.getIssuer().getValue(); + if (issuer == null || issuer.isEmpty()) { + throw new AuthException( + TSStatusCode.INIT_AUTH_ERROR, "OIDC provider metadata does not contain an issuer"); + } try { URL url = new URI(providerMetadata.getJWKSetURI().toString()).toURL(); - logger.debug("Using url {}", url); - return getProviderRsaJwk(url.openStream()); + return new ProviderContext(getProviderRsaJwk(url.openStream()), issuer, acceptedAudiences); } catch (IOException e) { throw new AuthException(TSStatusCode.INIT_AUTH_ERROR, "Unable to start the Auth", e); } @@ -161,14 +190,9 @@ public boolean login(String token, String password, final boolean useEncryptedPa try { claims = validateToken(token); } catch (JwtException e) { - logger.error("Unable to login the user with Username (token) {}", token, e); + logger.error("Unable to login the user with Username (token), {}", e.getMessage()); 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 String iotdbUsername = getUsername(claims); if (!super.listAllUsers().contains(iotdbUsername)) { @@ -193,13 +217,51 @@ public String getIoTDBUserName(String token) { } private Claims validateToken(String token) { - return Jwts.parser() - // Basically ignore the Expiration Date, if there is any??? - .clockSkewSeconds(Long.MAX_VALUE / 1000) - .verifyWith(providerKey) - .build() - .parseSignedClaims(token) - .getPayload(); + Claims claims = + Jwts.parser() + .clockSkewSeconds(MAX_CLOCK_SKEW_SECONDS) + .verifyWith(providerKey) + .build() + .parseSignedClaims(token) + .getPayload(); + validateClaims(claims); + return claims; + } + + private void validateClaims(Claims claims) { + if (expectedIssuer != null && !expectedIssuer.equals(claims.getIssuer())) { + throw new JwtException( + String.format("Unexpected issuer %s, expected %s", claims.getIssuer(), expectedIssuer)); + } + if (!acceptedAudiences.isEmpty() && !hasAcceptedAudience(claims.get("aud"))) { + throw new JwtException( + String.format( + "Unexpected audience %s, expected one of %s", claims.get("aud"), acceptedAudiences)); + } + } + + private boolean hasAcceptedAudience(Object audienceClaim) { + if (audienceClaim instanceof String) { + return acceptedAudiences.contains(audienceClaim); + } + if (audienceClaim instanceof List) { + for (Object audience : (List) audienceClaim) { + if (audience instanceof String && acceptedAudiences.contains(audience)) { + return true; + } + } + } + return false; + } + + private static Set parseAudiences(String configuredAudiences) { + if (configuredAudiences == null || configuredAudiences.trim().isEmpty()) { + return Collections.emptySet(); + } + return Arrays.stream(configuredAudiences.split(",")) + .map(String::trim) + .filter(audience -> !audience.isEmpty()) + .collect(Collectors.toCollection(HashSet::new)); } private String getUsername(Claims claims) { @@ -237,7 +299,7 @@ public boolean isAdmin(String token) { try { claims = validateToken(token); } catch (JwtException e) { - logger.warn("Unable to validate token {}!", token, e); + logger.warn("Unable to validate token! {}", e.getMessage()); return false; } } @@ -258,6 +320,18 @@ public boolean checkUserPrivileges(String userName, PrivilegeUnion union) throws return isAdmin(userName); } + private static class ProviderContext { + private final JSONObject jwk; + private final String issuer; + private final Set acceptedAudiences; + + private ProviderContext(JSONObject jwk, String issuer, Set acceptedAudiences) { + this.jwk = jwk; + this.issuer = issuer; + this.acceptedAudiences = acceptedAudiences; + } + } + @Override public void updateUserPassword(String userName, String newPassword) { throwUnsupportedOperationException(); diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java index a490107ded324..760cccb973baf 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java @@ -60,6 +60,7 @@ public class CommonConfig { // Open ID Secret private String openIdProviderUrl = ""; + private String openIdAudience = ""; // The authorizer provider class which extends BasicAuthorizer private String authorizerProvider = @@ -543,6 +544,14 @@ public void setOpenIdProviderUrl(String openIdProviderUrl) { this.openIdProviderUrl = openIdProviderUrl; } + public String getOpenIdAudience() { + return openIdAudience; + } + + public void setOpenIdAudience(String openIdAudience) { + this.openIdAudience = openIdAudience; + } + public String getAuthorizerProvider() { return authorizerProvider; } diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java index 8483d1425cfec..004b147938c6c 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java @@ -80,6 +80,8 @@ public void loadCommonProps(TrimProperties properties) throws IOException { // if using org.apache.iotdb.db.auth.authorizer.OpenIdAuthorizer, openID_url is needed. config.setOpenIdProviderUrl( properties.getProperty("openID_url", config.getOpenIdProviderUrl()).trim()); + config.setOpenIdAudience( + properties.getProperty("openID_audience", config.getOpenIdAudience()).trim()); config.setEncryptDecryptProvider( properties .getProperty( diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/auth/authorizer/LocalFileAuthorizerTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/auth/authorizer/LocalFileAuthorizerTest.java similarity index 96% rename from iotdb-core/datanode/src/test/java/org/apache/iotdb/db/auth/authorizer/LocalFileAuthorizerTest.java rename to iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/auth/authorizer/LocalFileAuthorizerTest.java index 194f33e8d67b3..90ee3afd86b85 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/auth/authorizer/LocalFileAuthorizerTest.java +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/auth/authorizer/LocalFileAuthorizerTest.java @@ -16,16 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.iotdb.db.auth.authorizer; +package org.apache.iotdb.commons.auth.authorizer; import org.apache.iotdb.commons.auth.AuthException; -import org.apache.iotdb.commons.auth.authorizer.BasicAuthorizer; -import org.apache.iotdb.commons.auth.authorizer.IAuthorizer; import org.apache.iotdb.commons.auth.entity.PrivilegeType; import org.apache.iotdb.commons.auth.entity.PrivilegeUnion; import org.apache.iotdb.commons.conf.CommonDescriptor; import org.apache.iotdb.commons.path.PartialPath; -import org.apache.iotdb.db.utils.EnvironmentUtils; import org.junit.After; import org.junit.Assert; @@ -51,16 +48,13 @@ public class LocalFileAuthorizerTest { @Before public void setUp() throws Exception { - EnvironmentUtils.envSetUp(); authorizer = BasicAuthorizer.getInstance(); authorizer.reset(); nodeName = new PartialPath("root.laptop.d1"); } @After - public void tearDown() throws Exception { - EnvironmentUtils.cleanEnv(); - } + public void tearDown() throws Exception {} @Test public void testLogin() throws AuthException { diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizerTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizerTest.java new file mode 100644 index 0000000000000..cf7fb53ae12b2 --- /dev/null +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizerTest.java @@ -0,0 +1,261 @@ +/* + * 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 agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.commons.auth.authorizer; + +import org.apache.iotdb.commons.conf.CommonConfig; +import org.apache.iotdb.commons.conf.CommonDescriptor; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import net.minidev.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Instant; +import java.util.Collections; +import java.util.Date; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class OpenIdAuthorizerTest { + + private final CommonConfig config = CommonDescriptor.getInstance().getConfig(); + private PrivateKey privateKey; + private JSONObject publicJwk; + + @Before + public void setUp() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + privateKey = keyPair.getPrivate(); + publicJwk = + new JSONObject( + new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) + .keyUse(KeyUse.SIGNATURE) + .keyID("datanode-openid-test-key") + .build() + .toJSONObject()); + } + + @After + public void tearDown() throws IOException {} + + @Test + public void loginWithJWT() throws Exception { + String jwt = createJwt(false); + OpenIdAuthorizer authorizer = new OpenIdAuthorizer(publicJwk); + assertTrue(authorizer.login(jwt, null, false)); + } + + @Test + public void isAdmin_hasAccess() throws Exception { + String jwt = createJwt(true); + OpenIdAuthorizer authorizer = new OpenIdAuthorizer(publicJwk); + assertTrue(authorizer.isAdmin(jwt)); + } + + @Test + public void isAdmin_noAdminClaim() throws Exception { + String jwt = createJwt(false); + OpenIdAuthorizer authorizer = new OpenIdAuthorizer(publicJwk); + assertFalse(authorizer.isAdmin(jwt)); + } + + @Test + public void testExpiredTokenRejectedByLoginAndIsAdmin() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + JSONObject jwk = + new JSONObject( + new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) + .privateKey(keyPair.getPrivate()) + .keyUse(KeyUse.SIGNATURE) + .keyID("expired-token-test-key") + .build() + .toJSONObject()); + + OpenIdAuthorizer authorizer = new OpenIdAuthorizer(jwk); + String expiredToken = + createSignedToken( + keyPair.getPrivate(), + new JWTClaimsSet.Builder() + .subject("attacker") + .expirationTime(Date.from(Instant.now().minusSeconds(3600))) + .claim( + "realm_access", + Collections.singletonMap( + "roles", + Collections.singletonList(OpenIdAuthorizer.IOTDB_ADMIN_ROLE_NAME)))); + + assertFalse(authorizer.login(expiredToken, "", false)); + assertFalse(authorizer.isAdmin(expiredToken)); + } + + @Test + public void testWrongIssuerRejected() throws Exception { + config.setOpenIdAudience("iotdb"); + KeyPair keyPair = generateKeyPair(); + HttpServer server = startProviderServer(keyPair); + String issuer = "http://127.0.0.1:" + server.getAddress().getPort() + "/"; + + try { + OpenIdAuthorizer authorizer = new OpenIdAuthorizer(issuer); + String token = + createSignedToken( + keyPair.getPrivate(), + new JWTClaimsSet.Builder() + .subject("attacker") + .issuer("https://evil.example/issuer") + .audience("iotdb") + .expirationTime(Date.from(Instant.now().plusSeconds(3600))) + .claim( + "realm_access", + Collections.singletonMap( + "roles", + Collections.singletonList(OpenIdAuthorizer.IOTDB_ADMIN_ROLE_NAME)))); + + assertFalse(authorizer.login(token, "", false)); + assertFalse(authorizer.isAdmin(token)); + } finally { + server.stop(0); + } + } + + @Test + public void testWrongAudienceRejected() throws Exception { + config.setOpenIdAudience("iotdb"); + KeyPair keyPair = generateKeyPair(); + HttpServer server = startProviderServer(keyPair); + String issuer = "http://127.0.0.1:" + server.getAddress().getPort() + "/"; + + try { + OpenIdAuthorizer authorizer = new OpenIdAuthorizer(issuer); + String token = + createSignedToken( + keyPair.getPrivate(), + new JWTClaimsSet.Builder() + .subject("attacker") + .issuer(issuer) + .audience("unrelated-client") + .expirationTime(Date.from(Instant.now().plusSeconds(3600))) + .claim( + "realm_access", + Collections.singletonMap( + "roles", + Collections.singletonList(OpenIdAuthorizer.IOTDB_ADMIN_ROLE_NAME)))); + + assertFalse(authorizer.login(token, "", false)); + assertFalse(authorizer.isAdmin(token)); + } finally { + server.stop(0); + } + } + + private KeyPair generateKeyPair() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); + } + + private String createSignedToken(PrivateKey privateKey, JWTClaimsSet.Builder claimsBuilder) + throws JOSEException { + SignedJWT signedJwt = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsBuilder.build()); + signedJwt.sign(new RSASSASigner(privateKey)); + return signedJwt.serialize(); + } + + private HttpServer startProviderServer(KeyPair keyPair) throws Exception { + JSONObject publicJwk = + new JSONObject( + new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) + .keyUse(KeyUse.SIGNATURE) + .keyID("openid-provider-test-key") + .build() + .toJSONObject()); + + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + String issuer = "http://127.0.0.1:" + server.getAddress().getPort() + "/"; + String metadata = + "{" + + "\"issuer\":\"" + + issuer + + "\"," + + "\"jwks_uri\":\"" + + issuer + + "jwks.json\"," + + "\"subject_types_supported\":[\"public\"]," + + "\"response_types_supported\":[\"code\"]," + + "\"id_token_signing_alg_values_supported\":[\"RS256\"]" + + "}"; + String jwks = "{\"keys\":[" + publicJwk.toJSONString() + "]}"; + + server.createContext( + "/.well-known/openid-configuration", exchange -> writeJson(exchange, metadata)); + server.createContext("/jwks.json", exchange -> writeJson(exchange, jwks)); + server.start(); + return server; + } + + private void writeJson(HttpExchange exchange, String json) throws IOException { + byte[] response = json.getBytes(java.nio.charset.StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length); + try (OutputStream outputStream = exchange.getResponseBody()) { + outputStream.write(response); + } + } + + private String createJwt(boolean hasAdminRole) throws JOSEException { + JWTClaimsSet.Builder claimsBuilder = + new JWTClaimsSet.Builder() + .subject("datanode-test-user") + .expirationTime(Date.from(Instant.now().plusSeconds(3600))) + .claim( + "realm_access", + Collections.singletonMap( + "roles", + hasAdminRole + ? Collections.singletonList(OpenIdAuthorizer.IOTDB_ADMIN_ROLE_NAME) + : Collections.singletonList("offline_access"))); + SignedJWT signedJwt = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsBuilder.build()); + signedJwt.sign(new RSASSASigner(privateKey)); + return signedJwt.serialize(); + } +}