From 6486580e45663a05b6fbd509b918d764d031d816 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Fri, 25 Aug 2023 19:37:48 +0000 Subject: [PATCH] Squashed commit of the following: commit 1e24bbbf8aa7329d30b082aa4927074d65f086b6 Author: Ryan Liang Date: Fri Aug 25 12:06:53 2023 -0700 Fixed the exception in keyutils Signed-off-by: Ryan Liang commit 4b406c5b5c38fbcee3a013f32d8abe1022e78934 Author: Ryan Liang Date: Fri Aug 25 11:41:14 2023 -0700 Change the null check right after the jwtparserbuilder Signed-off-by: Ryan Liang commit a805843a0bc167b508d265b1bee2049b07463a8e Author: Ryan Liang Date: Fri Aug 25 11:11:13 2023 -0700 Change to use copyof in getSecurityRoles in AuthCredentials class Signed-off-by: Ryan Liang commit 308f269cb3f893517ad8970bab9a9e6d52c27829 Author: Ryan Liang Date: Fri Aug 25 11:02:04 2023 -0700 Add the null or empty check for signingkey in keyUtils Signed-off-by: Ryan Liang commit 40eed32edcf702de306db4f1a0ec8d2312ce7ac3 Author: Ryan Liang Date: Fri Aug 25 09:55:19 2023 -0700 Fix V6 and V7 and lint Signed-off-by: Ryan Liang commit 3c761513eb03968b9564039d768e32e3ce96fe79 Author: Ryan Liang Date: Fri Aug 25 09:29:17 2023 -0700 Fix comment - Craig Signed-off-by: Ryan Liang commit 7f2fc19687ba77727141b3ac63feac7fc6d6fd56 Author: Ryan Liang Date: Thu Aug 24 23:01:24 2023 -0700 Fix some comments 08/24 Signed-off-by: Ryan Liang commit 4841b257d8f902c1e9418f0cd0c5af9ed5a5ec9c Author: Ryan Liang Date: Thu Aug 24 10:50:24 2023 -0700 Add the constant for defaut service in create obo endpoint Signed-off-by: Ryan Liang commit 477b50563cff470e822ce09ba9359f8af40589ff Author: Ryan Liang Date: Wed Aug 23 20:18:05 2023 -0700 Remove the unrelated line in AccountApiTest l77 Signed-off-by: Ryan Liang commit f42d2f5887d690ace1b0d16a54551047713291dc Author: Ryan Liang Date: Wed Aug 23 19:40:33 2023 -0700 Re-edit the error msg for createoboendpoint Signed-off-by: Ryan Liang commit a272ccbb39ae62f8f1ce5f1bbbce787067ab02e1 Author: Ryan Liang Date: Wed Aug 23 19:34:56 2023 -0700 Rename the obo endpoint Signed-off-by: Ryan Liang commit c0214736cfb08d9414152f6c6c6ea93882c46b7b Author: Ryan Liang Date: Wed Aug 23 19:30:45 2023 -0700 Correct the getClusterName() Signed-off-by: Ryan Liang commit 8b5158dcd3fcccac3aaadc044721c594137a37df Author: Ryan Liang Date: Wed Aug 23 19:11:07 2023 -0700 Use ClusterInfoHolder to pass clusterName Signed-off-by: Ryan Liang commit 336aa57f5fe7afee0f44a7c5f2968f094a0db2d0 Author: Ryan Liang Date: Wed Aug 23 18:18:42 2023 -0700 Change the error msg in jwtvendorTests too Signed-off-by: Ryan Liang commit 682379dd7ed049ce5e2b8505fa0987c46670c95a Author: Ryan Liang Date: Wed Aug 23 17:21:11 2023 -0700 Switch to assertThat in obo authenticator test Signed-off-by: Ryan Liang commit cb3406aae7aee63d8b0c363aa85586d92ea4da53 Author: Ryan Liang Date: Wed Aug 23 16:59:30 2023 -0700 Add comment in DynamicConfigModelV7 Signed-off-by: Ryan Liang commit 15c85303bd8e57493cfed2892f91c55f80c1664c Author: Ryan Liang Date: Wed Aug 23 16:47:13 2023 -0700 Change to assertThrows for obo authenticator tests Signed-off-by: Ryan Liang commit e56bf0147ecb0fabc3567c5a4401a64e70718700 Author: Ryan Liang Date: Wed Aug 23 16:39:24 2023 -0700 Rename to OnBehalfOfSettings in ConfigV6 Signed-off-by: Ryan Liang commit 62cfb4f920857c307b13f7b730c7456ee58a3c55 Author: Ryan Liang Date: Wed Aug 23 16:29:31 2023 -0700 Add comment for authentication failure in obo authenticator Signed-off-by: Ryan Liang commit d0ebe9130f4f78c4ded9b0c5709409427c672685 Author: Ryan Liang Date: Wed Aug 23 16:22:08 2023 -0700 Specify the error msg of missing signing key in jwtvendor Signed-off-by: Ryan Liang commit ca95380aaa3832fd6d5f089584da30e4c7ffe6ec Author: Ryan Liang Date: Wed Aug 23 16:12:46 2023 -0700 Refactor the jwtvendor expiry and set up upper limit Signed-off-by: Ryan Liang commit 884f7a13e42931ce878615bf5200e728f3b69937 Author: Ryan Liang Date: Wed Aug 23 13:51:11 2023 -0700 Flip the boolean logic of roleSecurityMode Signed-off-by: Ryan Liang commit e1021c258b630696b6ea8a218bcea194d2b8a321 Author: Ryan Liang Date: Wed Aug 23 13:36:05 2023 -0700 Refactor the bwc mode into roleSecurityMode Signed-off-by: Ryan Liang commit c1a825be68b339c600bfdea384b51d7cb37ae847 Author: Ryan Liang Date: Wed Aug 23 12:03:17 2023 -0700 Refactor in jwtVendor 1 Signed-off-by: Ryan Liang commit 8eac5cd1ab247ff3495cb4ebc17d1fd1a52f8146 Author: Ryan Liang Date: Wed Aug 23 09:53:24 2023 -0700 Change the comment in backend registry Signed-off-by: Ryan Liang commit b0ac41a56a566a3ac38771067b9824cc594fa944 Author: Ryan Liang Date: Tue Aug 22 17:58:15 2023 -0700 Revert the unrelated change in SecurityRestFilter Signed-off-by: Ryan Liang commit b64460dd5e575b46e4917d23e0d229c587a35892 Author: Ryan Liang Date: Tue Aug 22 17:53:32 2023 -0700 Remove stale function Signed-off-by: Ryan Liang commit 54bca2ac83a193af0773ca57b8c2daed1d821d66 Author: Ryan Liang Date: Tue Aug 22 17:36:05 2023 -0700 Refactor the obo endpoint Signed-off-by: Ryan Liang commit e429d7b79ef8e16f0b80fe34998d83de0855f5cb Author: Ryan Liang Date: Tue Aug 22 17:16:47 2023 -0700 Refactor the KeyUtils OBOAuthenticator and JwtAuthenticator with jwtParserBuilder Signed-off-by: Ryan Liang commit 72dcec1f702877fa5d9bc63a2bd48d7bdeb13166 Author: Ryan Liang Date: Tue Aug 22 16:22:42 2023 -0700 Some minor refactoring in obo authenticator Signed-off-by: Ryan Liang commit 6f0e79b638a3377922697c31de118a4c3ff05454 Author: Ryan Liang Date: Tue Aug 22 15:46:19 2023 -0700 Remove the malformed token warning for backendroles extraction Signed-off-by: Ryan Liang commit 1ba378e02726495b264a5f33dbb1903ba86fdfb7 Author: Ryan Liang Date: Tue Aug 22 15:42:38 2023 -0700 Refactor the EncryptionDecryptionUtil Signed-off-by: Ryan Liang commit b31555926c59aafe9a310d64918002b91d51c676 Author: Ryan Liang Date: Tue Aug 22 12:02:06 2023 -0700 Fix the exception type in JwtVendorTests Signed-off-by: Ryan Liang commit 6f498017eb8823600352b97c40d7314c9f672f74 Author: Ryan Liang Date: Tue Aug 22 11:35:10 2023 -0700 Remove the if condition in oboconfig for integ testing Signed-off-by: Ryan Liang commit 034aa345365f053b2e647e4529e3c91ea4f7e62b Author: Ryan Liang Date: Tue Aug 22 11:22:45 2023 -0700 Set up creatJwkException in exceptionUtils and apply that in JwtVendor constructor Signed-off-by: Ryan Liang commit 0f0478dcff2c900223cf2e7610fa4545ae739b23 Author: Ryan Liang Date: Tue Aug 22 10:14:55 2023 -0700 Rename the KeyUtils Signed-off-by: Ryan Liang commit a4e7affbf2f35cc8040c8f721f08c023e5e42a54 Author: Ryan Liang Date: Tue Aug 22 09:46:27 2023 -0700 Refactor the backendroles claim into br Signed-off-by: Ryan Liang commit 2ff746ee9df2e83f5be60386d3a4b374fc556596 Author: Ryan Liang Date: Tue Aug 22 09:31:50 2023 -0700 Fix lint Signed-off-by: Ryan Liang commit 9ce36dc690f60de97936dd7b646ae88b6795b758 Author: Ryan Liang Date: Tue Aug 22 01:11:24 2023 -0700 Refactor the OBO Authenticator part2 Signed-off-by: Ryan Liang commit e52c5ce103bf6d718017598889fda64e5e115ca5 Author: Ryan Liang Date: Tue Aug 22 00:55:33 2023 -0700 Refactor the backendrole extraction in oboauthenticator Signed-off-by: Ryan Liang commit 9c9e0600c37bbe0466dbb87df604ba4ed3698312 Author: Ryan Liang Date: Tue Aug 22 00:41:51 2023 -0700 Refactor the role extraction in oboauthenticator Signed-off-by: Ryan Liang commit 387027bbca257064f9f0417b05182de93ebb0588 Author: Ryan Liang Date: Tue Aug 22 00:18:31 2023 -0700 Refactor the logic in JwtVendor Signed-off-by: Ryan Liang commit 267255ce0d427a4c5016c08ee62a50c1edc94ba0 Author: Ryan Liang Date: Tue Aug 22 00:01:15 2023 -0700 Add integration test case for obo permission Signed-off-by: Ryan Liang commit 93bc8c6ff7075ffd0c43f70213a6703d81a0f43b Author: Ryan Liang Date: Mon Aug 21 19:18:49 2023 -0700 Remove unused constants variable in OBO authenticator Signed-off-by: Ryan Liang commit 878a1077e0a826325ca8a89c3ecddddbd5e2128b Author: Ryan Liang Date: Mon Aug 21 19:10:27 2023 -0700 Refactor the encryptiondecryptionutilstests Signed-off-by: Ryan Liang commit 1c1bae694e7368fc95b926ef1c109f8c007b1d0f Author: Ryan Liang Date: Mon Aug 21 14:53:45 2023 -0700 Remove the null check in oboconfig Signed-off-by: Ryan Liang commit 7e3824e8ae50b4367b5d4664c0925752c7da92d4 Author: Ryan Liang Date: Mon Aug 21 14:44:07 2023 -0700 Remove the wording of seconds in obo endpoint and make the expiry into configconstants Signed-off-by: Ryan Liang commit a1265126868f9b0d0676cb44b02542d7f4e3b50e Author: Ryan Liang Date: Mon Aug 21 12:48:33 2023 -0700 Use constant util in Obo integration test Signed-off-by: Ryan Liang commit e5a32c62b7a8bdba2148d4ef7543d77b6d5a683b Author: Ryan Liang Date: Mon Aug 21 12:13:50 2023 -0700 Rename the obo endpoint path to generateobotoekn Signed-off-by: Ryan Liang commit e09a902c17c15117ffef5d527f2d9efe51feacb0 Author: Ryan Liang Date: Mon Aug 21 09:15:53 2023 -0700 Remove the unused instance in configV6 Signed-off-by: Ryan Liang commit af8aaa7446c7e234dbc4d9f3793da626ccb408f7 Author: Ryan Liang Date: Fri Aug 18 13:05:04 2023 -0700 Fix the lint Signed-off-by: Ryan Liang commit 9103b2308573bc15e8d0a2bf420a3befa84feaf2 Author: Ryan Liang Date: Fri Aug 18 13:01:56 2023 -0700 Add permission obo/create for accessing create obo endpoint Signed-off-by: Ryan Liang commit 234921328d601a7a7777428682d43de193f1cc90 Author: Ryan Liang Date: Thu Aug 17 15:54:38 2023 -0700 Change the name into keyUtils with the s Signed-off-by: Ryan Liang commit dae0ac7e22307d1713df3d05ef562e2163363d38 Author: Ryan Liang Date: Thu Aug 17 15:48:12 2023 -0700 Switch to try/catch + assertEquals for JwtVendorTest Signed-off-by: Ryan Liang commit d918d7a4cbb4d2943dc9f727f6163188982c15ee Author: Ryan Liang Date: Thu Aug 17 15:39:46 2023 -0700 Change the JwtVendorTest with manually built-up assertThrow Signed-off-by: Ryan Liang commit f47026bddacfee87e49503b809de08cce1318265 Author: Ryan Liang Date: Thu Aug 17 14:35:13 2023 -0700 Fix the typo in exceptionUtils Signed-off-by: Ryan Liang commit 43b8d5ded8d5590243aa7f4337ecb94eeac8b69a Author: Ryan Liang Date: Thu Aug 17 14:33:22 2023 -0700 Remove stacktrace debug statement in OBOAutehnticator Signed-off-by: Ryan Liang commit a2c6db19fd25fe55059458c6b2af80d597ab858a Author: Ryan Liang Date: Thu Aug 17 13:04:30 2023 -0700 Change some of the methods name into camle case instead of snake case Signed-off-by: Ryan Liang commit ee79b49334d176e8f039cb8c64e77853b5028393 Author: Ryan Liang Date: Thu Aug 17 11:55:11 2023 -0700 Add unit tests for EncryptionDecryptionUtil Signed-off-by: Ryan Liang commit 3ebff2bfb2be617c17c3efcc1ccfde675b85e610 Author: Ryan Liang Date: Thu Aug 17 09:43:43 2023 -0700 Work around for not set static cluster service Signed-off-by: Ryan Liang commit a4efad62fbc109b176bce4b4c31e9b5abd0cd413 Author: Ryan Liang Date: Wed Aug 16 13:31:57 2023 -0700 Modify the getDynamicOnBehalfOfSettings() to return settings.Empty if there is no changes Signed-off-by: Ryan Liang commit e23d757a93282777aa3188fdbb0d68eeb2459e10 Author: Ryan Liang Date: Wed Aug 16 12:26:39 2023 -0700 Encapsulate the logic for endpoints access checking into a method Signed-off-by: Ryan Liang commit be261487584f76cba4ce5510d6617a804405a0d3 Author: Ryan Liang Date: Wed Aug 16 12:09:25 2023 -0700 Remove useless comments Signed-off-by: Ryan Liang commit bef85da1de64fd5f2c525c0e0eb7bf17af9704aa Author: Ryan Liang Date: Wed Aug 16 11:57:39 2023 -0700 Remove the enforcing of token type for OBO auth Signed-off-by: Ryan Liang commit 1f794312a053ab799870931d5584b34fa362042e Author: Ryan Liang Date: Wed Aug 16 11:45:45 2023 -0700 Change the field name reason in obo endpoint to description Signed-off-by: Ryan Liang commit 1f12e5e183cba894918b7cecb91c5fb77075635d Author: Ryan Liang Date: Wed Aug 16 11:15:56 2023 -0700 Change the backendrole check's claim name into br Signed-off-by: Ryan Liang commit b2c7d75db2f3805b91d76335bb43845fdb41f41c Author: Ryan Liang Date: Tue Aug 15 23:30:25 2023 -0700 Address some comment 2 Signed-off-by: Ryan Liang commit d79973c282006f0e59cacc7634edb3ca56e16fe0 Author: Ryan Liang Date: Tue Aug 15 23:24:16 2023 -0700 Address some comment 1 Signed-off-by: Ryan Liang commit 8a96cab07cdffd754fb43a91dc1e17033703be77 Author: Sam <128482925+samuelcostae@users.noreply.github.com> Date: Fri Aug 18 14:43:07 2023 +0100 Feature/extensions bwc setting (#3180) ### Description This Draft PR includes the new setting bwcPluginMode (backward compatible plugin mode for extensions ) ### Issues Resolved #2616 Is this a backport? If so, please add backport PR # and/or commits # ### Testing [Please provide details of testing done: unit testing, integration testing and manual testing] ### Check List - [ ] New functionality includes testing - [ ] New functionality has been documented - [x] Commits are signed per the DCO using --signoff By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin). --------- Signed-off-by: Sam commit 91f447864b5ee02ff0b2b9ea169e39eadb52dfd9 Merge: e42e4d35 88b6d23f Author: Ryan Liang Date: Mon Aug 14 23:30:37 2023 -0700 Merge branch 'main' into feature/extensions commit e42e4d3568d25b00507f88a0fc533db7664bacfb Author: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Mon Aug 14 10:12:58 2023 -0700 [Feature/Extension] Remove hostmapping from create OBO endpoint. (#3161) Signed-off-by: Ryan Liang commit ade34b4f83a8cf43e00b9c874b04cd58a3728b17 Merge: 6d8e0e21 05f12d8b Author: Ryan Liang Date: Fri Aug 11 11:37:21 2023 -0700 Merge branch 'main' into feature/extensions commit 6d8e0e217862fecbc23f7e2b10c2a773dd3395a6 Merge: 493b53f1 3139c184 Author: Ryan Liang Date: Thu Aug 10 21:11:43 2023 -0700 Merge branch 'main' into feature/extensions commit 493b53f11ffe3c2891188a6f4df28ebcc0853ffc Merge: 30cf5b11 46989b57 Author: Ryan Liang Date: Wed Aug 9 12:00:14 2023 -0700 Merge branch 'main' into feature/extensions commit 30cf5b1138503c8af7b2818e6a478b58b6f17c23 Author: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Wed Aug 9 10:10:24 2023 -0700 [Feature/Extension] Add cluster id check for OBO Authenticator (#3117) --------- Signed-off-by: Ryan Liang commit 058f8ec011b4b16ff4f03245072f8596b5fea295 Merge: d643fb28 6cc90e65 Author: Ryan Liang Date: Mon Aug 7 12:33:57 2023 -0700 Merge branch 'main' into feature/extensions commit d643fb285fa7173357627f2756ec42fd56cb7145 Author: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Fri Aug 4 22:57:18 2023 -0700 [Feature/Extension] Restrict OBO token's usage for certain endpoints (#3008) Signed-off-by: Ryan Liang commit 2319059cc10a40bd1ce0a714aa88345e8bd7dc1f Merge: d634d60d 527495dd Author: Ryan Liang Date: Fri Aug 4 08:50:34 2023 -0700 Merge branch 'main' into feature/extensions commit d634d60d2c7aa85266d6d8de5d2b9737c4c3317d Author: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Wed Aug 2 13:09:03 2023 -0700 [Feature/Extension] Add configuration of disable OBO (#3047) Signed-off-by: Ryan Liang commit df3dba31930be4ee63f240fcfa381686c6ea3c86 Merge: 1268dee6 5384272a Author: Ryan Liang Date: Wed Aug 2 09:42:04 2023 -0700 Merge branch 'main' into feature/extensions commit 1268dee6e5855310311527952784af49b1346bce Merge: a9451dda 8063e1b6 Author: Ryan Liang Date: Tue Jul 25 11:23:05 2023 -0700 Merge branch 'main' into feature/extensions commit a9451dda6657fc3379fb00ab2798a8f55d117bad Merge: 671c7725 59e26578 Author: Ryan Liang Date: Mon Jul 24 13:30:37 2023 -0700 Merge branch 'main' into feature/extensions commit 671c772568cda59705f9f42c399a597cd62c4619 Merge: 67515bcd f1be2d77 Author: Ryan Liang Date: Tue Jul 18 09:04:25 2023 -0700 Merge branch 'main' into feature/extensions commit 67515bcd293be8f09293b10f2f3ab8eb3fcd9094 Merge: 88f32e98 0e6608d9 Author: Ryan Liang Date: Thu Jul 13 11:18:07 2023 -0700 Merge branch 'main' into feature/extensions commit 88f32e98304aa1aedfbcba6f471b74f6457262df Author: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Fri Jul 7 11:42:21 2023 -0700 [Feature/Extension] Add oboauthcbackend registry and set up e2e endpoint testing flow (#2857) * Add OBO Authbackend Signed-off-by: Peter Nied Signed-off-by: Ryan Liang Co-authored-by: Peter Nied commit 8c3c6394d1479890f859e4855b6e4245a8f532af Merge: 21891d72 4eef662f Author: Ryan Liang Date: Mon Jul 3 10:34:27 2023 -0700 Merge branch 'main' into feature/extensions commit 21891d72b87cfdddaf3a493101dd058386a3fe4c Merge: 8ad24ad7 7546c056 Author: Ryan Liang Date: Thu Jun 29 13:28:12 2023 -0700 Merge branch 'feature-branch-sync-629' into feature/extensions commit 8ad24ad794beaf5335591dbfc55c32c196a7ece6 Author: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Thu Jun 29 13:23:04 2023 -0700 Revert "Feature branch sync 06/29/2023 (#2918)" (#2920) This reverts commit 748a71139695422eccf253112321a7a5f6b9b83d. commit 748a71139695422eccf253112321a7a5f6b9b83d Author: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Thu Jun 29 12:37:08 2023 -0700 Feature branch sync 06/29/2023 (#2918) * add search model group permission to ml_read_access role (#2855) * add search model group permission to ml_read_access role Signed-off-by: Bhavana Ramaram * IntegrationTest spotless (#2863) Signed-off-by: Stephen Crawford * Format everything (#2866) * Use boucycastle PEM reader instead of reg expression (#2864) Use BouncyCastle PEMReader instead of regular expression to read and parse private key pem files. Signed-off-by: Andrey Pleskach * Adding field level security test cases for FlatFields (#2876) Signed-off-by: Peter Nied * Update snappy to 1.1.10.1 and guava to 32.0.1-jre (#2886) * Update snappy to 1.1.10.1 and guava to 32.0.1-jre Signed-off-by: Craig Perkins * Upgrade kafka to 3.5.0 Signed-off-by: Craig Perkins * Force snappy Signed-off-by: Craig Perkins * Add runtime dependency on org.scala-lang.modules:scala-java8-compat_3:1.0.2 to fix issue with KafkaSinkTest Signed-off-by: Craig Perkins --------- Signed-off-by: Craig Perkins * Role permissions order tool and workflow (#2733) * Check Permissions Order tool and workflow Adds a NodeJS tool that can inspect yaml role definitions, check if they are in alphabetical order, correct them if required. Signed-off-by: Peter Nied * Apply fixes to roles.yml files Signed-off-by: Peter Nied * Fixing busted test, adding findArrayInJson for response bodies Signed-off-by: Peter Nied --------- Signed-off-by: Peter Nied Signed-off-by: Peter Nied * Misc changes (#2902) Moved isStatic and isReserved methods to the SecurityDynamicConfiguration class Signed-off-by: Andrey Pleskach * Update triaging guidelines (#2899) * Update triaging guidelines Signed-off-by: Stephen Crawford Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> * fix cluster perm classification for msearch template (#2892) * fix cluster perm classification for msearch template Signed-off-by: Derek Ho * move test to unit test file Signed-off-by: Derek Ho * fully revert integration test file Signed-off-by: Derek Ho * Update src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> * spotless Signed-off-by: Derek Ho --------- Signed-off-by: Derek Ho Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Co-authored-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> * [Doc] Add architecture document (#2869) * Add initial architecture document Signed-off-by: Peter Nied * [Enhancement] Parallel test jobs for CI (#2861) * Split multiple tests into separate gradle tasks. * Tasks are configured in "splitTestConfig" map in build.gradle file. Map allows to use all patterns from TestFilter like: includeTestsMatching, excludeTestsMatching, includeTest etc. * Tasks are automatically generated from "splitTestConfig" map. * Two new Gradle tasks: listTasksAsJSON and listTasksAsParam to output task names to console. First one outputs them as a JSON and second - in gradlew "-x " format to use in CLI. * Patterns included in tasks are automatically excluded from main "test" task but at the same time generated tasks are dependencies for "test". Running "gradlew test" will run whole suite at once. * CI pipeline has been configured to accomodate all changes. * New 'master' task to generate list of jobs to run in parallel. * Updated matrix strategy to include task name to start. Signed-off-by: Pawel Gudel * Bump BouncyCastle from jdk15on to jdk15to18 (#2901) jdk15to18 contains fix for - CVE-2023-33201 - Medium Severity Vulnerability Signed-off-by: Andrey Pleskach * Spotless Apply Signed-off-by: Ryan Liang --------- Signed-off-by: Bhavana Ramaram Signed-off-by: Stephen Crawford Signed-off-by: Andrey Pleskach Signed-off-by: Peter Nied Signed-off-by: Craig Perkins Signed-off-by: Peter Nied Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Signed-off-by: Derek Ho Signed-off-by: Pawel Gudel Signed-off-by: Ryan Liang Co-authored-by: Bhavana Ramaram Co-authored-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Co-authored-by: Andrey Pleskach Co-authored-by: Peter Nied Co-authored-by: Craig Perkins Co-authored-by: Derek Ho Co-authored-by: pawel-gudel-eliatra <136344230+pawel-gudel-eliatra@users.noreply.github.com> commit 7546c056d97b47d243108e9154f7be12afd653d2 Author: Ryan Liang Date: Thu Jun 29 11:50:58 2023 -0700 Spotless Apply Signed-off-by: Ryan Liang commit 81b781853160871c81e9e5ba3301f10ae76b358d Author: Andrey Pleskach Date: Thu Jun 29 15:54:21 2023 +0200 Bump BouncyCastle from jdk15on to jdk15to18 (#2901) jdk15to18 contains fix for - CVE-2023-33201 - Medium Severity Vulnerability Signed-off-by: Andrey Pleskach commit 95efddd472888b9342d4b4ce7b83e9b0e3be7ffc Author: pawel-gudel-eliatra <136344230+pawel-gudel-eliatra@users.noreply.github.com> Date: Wed Jun 28 22:41:46 2023 +0200 [Enhancement] Parallel test jobs for CI (#2861) * Split multiple tests into separate gradle tasks. * Tasks are configured in "splitTestConfig" map in build.gradle file. Map allows to use all patterns from TestFilter like: includeTestsMatching, excludeTestsMatching, includeTest etc. * Tasks are automatically generated from "splitTestConfig" map. * Two new Gradle tasks: listTasksAsJSON and listTasksAsParam to output task names to console. First one outputs them as a JSON and second - in gradlew "-x " format to use in CLI. * Patterns included in tasks are automatically excluded from main "test" task but at the same time generated tasks are dependencies for "test". Running "gradlew test" will run whole suite at once. * CI pipeline has been configured to accomodate all changes. * New 'master' task to generate list of jobs to run in parallel. * Updated matrix strategy to include task name to start. Signed-off-by: Pawel Gudel commit 766389b6028a3ec9f46125dbde6eef77bad5590b Author: Peter Nied Date: Wed Jun 28 15:28:11 2023 -0500 [Doc] Add architecture document (#2869) * Add initial architecture document Signed-off-by: Peter Nied commit c1d212722d503f723b412c016cb14b85b8990980 Author: Derek Ho Date: Wed Jun 28 15:21:04 2023 -0400 fix cluster perm classification for msearch template (#2892) * fix cluster perm classification for msearch template Signed-off-by: Derek Ho * move test to unit test file Signed-off-by: Derek Ho * fully revert integration test file Signed-off-by: Derek Ho * Update src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> * spotless Signed-off-by: Derek Ho --------- Signed-off-by: Derek Ho Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Co-authored-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> commit 37f277ede6cf4f10f1b3ebe013dc8f285e4e2820 Author: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Date: Mon Jun 26 15:28:13 2023 -0400 Update triaging guidelines (#2899) * Update triaging guidelines Signed-off-by: Stephen Crawford Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> commit 926bdda4ab665349d271451a53d972142a758ee0 Author: Andrey Pleskach Date: Mon Jun 26 20:09:39 2023 +0200 Misc changes (#2902) Moved isStatic and isReserved methods to the SecurityDynamicConfiguration class Signed-off-by: Andrey Pleskach commit 9cd0198f63fffcc4bf696c08801f71ba09278e4e Author: Peter Nied Date: Mon Jun 26 10:04:39 2023 -0500 Role permissions order tool and workflow (#2733) * Check Permissions Order tool and workflow Adds a NodeJS tool that can inspect yaml role definitions, check if they are in alphabetical order, correct them if required. Signed-off-by: Peter Nied * Apply fixes to roles.yml files Signed-off-by: Peter Nied * Fixing busted test, adding findArrayInJson for response bodies Signed-off-by: Peter Nied --------- Signed-off-by: Peter Nied Signed-off-by: Peter Nied commit 4bb144faf6dc36852eb6d1e70c919023967430b5 Author: Craig Perkins Date: Wed Jun 21 09:31:47 2023 -0400 Update snappy to 1.1.10.1 and guava to 32.0.1-jre (#2886) * Update snappy to 1.1.10.1 and guava to 32.0.1-jre Signed-off-by: Craig Perkins * Upgrade kafka to 3.5.0 Signed-off-by: Craig Perkins * Force snappy Signed-off-by: Craig Perkins * Add runtime dependency on org.scala-lang.modules:scala-java8-compat_3:1.0.2 to fix issue with KafkaSinkTest Signed-off-by: Craig Perkins --------- Signed-off-by: Craig Perkins commit c71d9b353386bce1776fafef55401aaeecdbb2d4 Author: Peter Nied Date: Tue Jun 20 13:36:01 2023 -0500 Adding field level security test cases for FlatFields (#2876) Signed-off-by: Peter Nied commit e3b4f8f91296cc76a80d27d14621a8170b187124 Author: Andrey Pleskach Date: Mon Jun 19 16:18:28 2023 +0200 Use boucycastle PEM reader instead of reg expression (#2864) Use BouncyCastle PEMReader instead of regular expression to read and parse private key pem files. Signed-off-by: Andrey Pleskach commit ef6224c6bb9d0893b9dd8d186515e19698b1a28a Author: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Date: Thu Jun 15 23:32:10 2023 -0400 Format everything (#2866) commit ef048a2bb26e0520d53717df334a0ee627aa7442 Author: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Date: Thu Jun 15 11:57:25 2023 -0400 IntegrationTest spotless (#2863) Signed-off-by: Stephen Crawford commit b6bfb114ea247f84ca986c28936feb1eb622286c Author: Bhavana Ramaram Date: Tue Jun 13 17:00:34 2023 -0700 add search model group permission to ml_read_access role (#2855) * add search model group permission to ml_read_access role Signed-off-by: Bhavana Ramaram commit 26244e901df691e44c6212cd3ad4f480c15307f0 Merge: 56e77fe1 1691ca7c Author: Craig Perkins Date: Tue Jun 13 16:14:33 2023 -0400 Merge branch 'format-feature-extensions-apply' into feature/extensions commit 1691ca7c3a6f0a555b6fda743741557604d01092 Merge: efcadd46 2e263b81 Author: Craig Perkins Date: Tue Jun 13 16:05:59 2023 -0400 Merge branch 'main' into format-feature-extensions-apply commit efcadd464f74e235145ff58eed4c01758392126a Merge: 1a09a87c ceb5ad29 Author: Craig Perkins Date: Fri Jun 9 10:16:02 2023 -0400 Merge branch 'main' into format-feature-extensions-apply commit 1a09a87c89434e3beb84702df2ddd64de4e095b7 Author: Craig Perkins Date: Fri Jun 9 09:57:42 2023 -0400 Run spotlessApply Signed-off-by: Craig Perkins commit 01917ff2e711735641e4b8c74df9b1041a2f3b7a Author: Craig Perkins Date: Fri Jun 9 09:55:09 2023 -0400 Remove other spotless section Signed-off-by: Craig Perkins commit c83ad28000fb26c46a19f5d6de1226c0d1503b65 Author: Craig Perkins Date: Fri Jun 9 09:53:35 2023 -0400 Add formatting changes in feature/extensions Signed-off-by: Craig Perkins commit 56e77fe1ad41879b85cfc1194980ed991ea084db Merge: fa0fcc3b 33aebb96 Author: Ryan Liang Date: Wed May 24 11:31:54 2023 -0700 Merge branch 'main' into feature/extensions commit fa0fcc3be29f3c6c61c9d011508b0fb0b87c34a2 Author: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Tue May 16 10:55:57 2023 -0700 [Feature/Extension] Rename the term 'extension' into 'on_behalf_of' (#2774) Signed-off-by: Ryan Liang commit f1cee3b966a80691f78f5378eb8047b9f87795b6 Merge: 95f9c77e 15860b65 Author: Ryan Liang Date: Tue May 16 09:33:59 2023 -0700 Merge branch 'main' into feature/extensions commit 95f9c77ea2cff30c60d783de041e3386bb43f634 Author: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Thu May 11 11:50:12 2023 -0700 [Security/Extension] Extension Authentication Backend (#2672) * Extension Authentication-backend Signed-off-by: Ryan Liang commit 8f02d8d89ca083552183be60f48a106768eba6f6 Merge: 9515181f 9d758f91 Author: Ryan Liang Date: Wed May 10 12:12:20 2023 -0700 Merge branch 'main' into feature/extensions commit 9515181f1c41a6e6c7fd1e3cc24f0868e4315062 Author: Ryan Liang Date: Wed May 10 09:51:26 2023 -0700 Fix the conflicts Signed-off-by: Ryan Liang commit 06055c3cea119183624340fb31d7933cc6189f91 Merge: df75a377 f4def32f Author: Ryan Liang Date: Wed May 10 09:34:05 2023 -0700 merge main into security extension feature branch commit df75a377ba3bd26f7ba12001e18d478370624575 Author: MaciejMierzwa Date: Tue May 2 15:44:04 2023 +0200 Extensions config for JWT signing/encryption key (#2671) * Extensions config for JWT signing/encryption key Signed-off-by: Maciej Mierzwa commit d4e5f1fe645a5997b5813af3b7e7be7b427d32cc Merge: 4da62c3a 6997f97f Author: Ryan Liang Date: Thu Apr 27 07:05:39 2023 -0700 Merge branch 'main' into feature/extensions commit 4da62c3a9564b6dc5ecdfae95d773cdad03251e1 Merge: 73ab1fc2 6ace852f Author: Craig Perkins Date: Tue Apr 25 09:38:52 2023 -0400 Merge branch 'main' into feature/extensions commit 73ab1fc2f61b8dffc8c52505fd34a1cc1aa6c585 Author: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Thu Apr 6 13:38:19 2023 -0700 [Security/Extension] Role encryption/decryption (#2620) * Encryption/Decryption of `roles` Signed-off-by: Ryan Liang commit 1681823c960233e7089258aba995c284a547190b Author: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Fri Mar 31 06:58:56 2023 -0700 [Security/Extension] JWT Vendor for extensions (#2567) * JWT Vendor for extensions Signed-off-by: Ryan Liang Signed-off-by: Peter Nied --- .../http/OnBehalfOfJwtAuthenticationTest.java | 170 ++++++++ .../test/framework/OnBehalfOfConfig.java | 51 +++ .../test/framework/TestSecurityConfig.java | 15 + .../test/framework/cluster/LocalCluster.java | 6 + .../framework/cluster/TestRestClient.java | 22 + src/integrationTest/resources/config.yml | 5 + .../auth/http/jwt/HTTPJwtAuthenticator.java | 76 +--- .../security/OpenSearchSecurityPlugin.java | 21 +- .../onbehalf/CreateOnBehalfOfTokenAction.java | 179 ++++++++ .../security/auth/BackendRegistry.java | 7 + .../internal/NoOpAuthenticationBackend.java | 4 +- .../jwt/EncryptionDecryptionUtil.java | 72 ++++ .../security/authtoken/jwt/JwtVendor.java | 171 ++++++++ .../configuration/ClusterInfoHolder.java | 9 + .../http/OnBehalfOfAuthenticator.java | 255 +++++++++++ .../securityconf/DynamicConfigFactory.java | 4 +- .../securityconf/DynamicConfigModel.java | 3 + .../securityconf/DynamicConfigModelV6.java | 5 + .../securityconf/DynamicConfigModelV7.java | 39 +- .../securityconf/impl/v6/ConfigV6.java | 48 +++ .../securityconf/impl/v7/ConfigV7.java | 49 +++ .../security/ssl/util/ExceptionUtils.java | 12 + .../security/support/ConfigConstants.java | 6 + .../security/user/AuthCredentials.java | 22 + .../opensearch/security/util/KeyUtils.java | 88 ++++ .../jwt/EncryptionDecryptionUtilsTest.java | 88 ++++ .../security/authtoken/jwt/JwtVendorTest.java | 185 ++++++++ .../http/OnBehalfOfAuthenticatorTest.java | 400 ++++++++++++++++++ .../restapi/securityconfig_nondefault.json | 7 +- 29 files changed, 1949 insertions(+), 70 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java create mode 100644 src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java create mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java create mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java create mode 100644 src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java create mode 100644 src/main/java/org/opensearch/security/util/KeyUtils.java create mode 100644 src/test/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtilsTest.java create mode 100644 src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java create mode 100644 src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java new file mode 100644 index 0000000000..4fc5b059ea --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -0,0 +1,170 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.http; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.message.BasicHeader; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.OnBehalfOfConfig; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class OnBehalfOfJwtAuthenticationTest { + + public static final String POINTER_USERNAME = "/user_name"; + + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + private static Boolean oboEnabled = true; + private static final String signingKey = Base64.getEncoder() + .encodeToString( + "jwt signing key for an on behalf of token authentication backend for testing of OBO authentication".getBytes( + StandardCharsets.UTF_8 + ) + ); + private static final String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); + public static final String ADMIN_USER_NAME = "admin"; + public static final String OBO_USER_NAME_WITH_PERM = "obo_user"; + public static final String OBO_USER_NAME_NO_PERM = "obo_user_no_perm"; + public static final String DEFAULT_PASSWORD = "secret"; + public static final String NEW_PASSWORD = "testPassword123!!"; + public static final String OBO_TOKEN_REASON = "{\"reason\":\"Test generation\"}"; + public static final String OBO_ENDPOINT_PREFIX = "_plugins/_security/api/generateonbehalfoftoken"; + public static final String OBO_DESCRIPTION = "{\"description\":\"Testing\", \"service\":\"self-issued\"}"; + public static final String CURRENT_AND_NEW_PASSWORDS = "{ \"current_password\": \"" + + DEFAULT_PASSWORD + + "\", \"password\": \"" + + NEW_PASSWORD + + "\" }"; + + protected final static TestSecurityConfig.User OBO_USER = new TestSecurityConfig.User(OBO_USER_NAME_WITH_PERM).roles( + new TestSecurityConfig.Role("obo_access_role").clusterPermissions("security:obo/create") + ); + + protected final static TestSecurityConfig.User OBO_USER_NO_PERM = new TestSecurityConfig.User(OBO_USER_NAME_NO_PERM).roles( + new TestSecurityConfig.Role("obo_user_no_perm") + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .users(ADMIN_USER, OBO_USER, OBO_USER_NO_PERM) + .nodeSettings( + Map.of(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true, SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_admin__all_access")) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .onBehalfOf(new OnBehalfOfConfig().oboEnabled(oboEnabled).signingKey(signingKey).encryptionKey(encryptionKey)) + .build(); + + @Test + public void shouldAuthenticateWithOBOTokenEndPoint() { + String oboToken = generateOboToken(ADMIN_USER_NAME, DEFAULT_PASSWORD); + Header adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + oboToken); + authenticateWithOboToken(adminOboAuthHeader, ADMIN_USER_NAME, 200); + } + + @Test + public void shouldNotAuthenticateWithATemperedOBOToken() { + String oboToken = generateOboToken(ADMIN_USER_NAME, DEFAULT_PASSWORD); + oboToken = oboToken.substring(0, oboToken.length() - 1); // tampering the token + Header adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + oboToken); + authenticateWithOboToken(adminOboAuthHeader, ADMIN_USER_NAME, 401); + } + + @Test + public void shouldNotAuthenticateForUsingOBOTokenToAccessOBOEndpoint() { + String oboToken = generateOboToken(ADMIN_USER_NAME, DEFAULT_PASSWORD); + Header adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + oboToken); + + try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { + TestRestClient.HttpResponse response = client.getOnBehalfOfToken(OBO_DESCRIPTION, adminOboAuthHeader); + response.assertStatusCode(401); + } + } + + @Test + public void shouldNotAuthenticateForUsingOBOTokenToAccessAccountEndpoint() { + String oboToken = generateOboToken(ADMIN_USER_NAME, DEFAULT_PASSWORD); + Header adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + oboToken); + + try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { + TestRestClient.HttpResponse response = client.changeInternalUserPassword(CURRENT_AND_NEW_PASSWORDS, adminOboAuthHeader); + response.assertStatusCode(401); + } + } + + @Test + public void shouldAuthenticateForNonAdminUserWithOBOPermission() { + String oboToken = generateOboToken(OBO_USER_NAME_WITH_PERM, DEFAULT_PASSWORD); + Header oboAuthHeader = new BasicHeader("Authorization", "Bearer " + oboToken); + authenticateWithOboToken(oboAuthHeader, OBO_USER_NAME_WITH_PERM, 200); + } + + @Test + public void shouldNotAuthenticateForNonAdminUserWithoutOBOPermission() { + try (TestRestClient client = cluster.getRestClient(OBO_USER_NO_PERM)) { + assertThat(client.post(OBO_ENDPOINT_PREFIX).getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED)); + } + } + + private String generateOboToken(String username, String password) { + try (TestRestClient client = cluster.getRestClient(username, password)) { + client.assertCorrectCredentials(username); + TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_TOKEN_REASON); + response.assertStatusCode(200); + Map oboEndPointResponse = response.getBodyAs(Map.class); + assertThat( + oboEndPointResponse, + allOf(aMapWithSize(3), hasKey("user"), hasKey("authenticationToken"), hasKey("durationSeconds")) + ); + return oboEndPointResponse.get("authenticationToken").toString(); + } + } + + private void authenticateWithOboToken(Header authHeader, String expectedUsername, int expectedStatusCode) { + try (TestRestClient client = cluster.getRestClient(authHeader)) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(expectedStatusCode); + if (expectedStatusCode == 200) { + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(expectedUsername)); + } else { + Assert.assertTrue(response.getBody().contains("Unauthorized")); + } + } + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java new file mode 100644 index 0000000000..63e1544f98 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.test.framework; + +import java.io.IOException; + +import org.apache.commons.lang3.StringUtils; + +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class OnBehalfOfConfig implements ToXContentObject { + private Boolean oboEnabled; + private String signing_key; + private String encryption_key; + + public OnBehalfOfConfig oboEnabled(Boolean oboEnabled) { + this.oboEnabled = oboEnabled; + return this; + } + + public OnBehalfOfConfig signingKey(String signing_key) { + this.signing_key = signing_key; + return this; + } + + public OnBehalfOfConfig encryptionKey(String encryption_key) { + this.encryption_key = encryption_key; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, ToXContent.Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("enabled", oboEnabled); + xContentBuilder.field("signing_key", signing_key); + if (StringUtils.isNoneBlank(encryption_key)) { + xContentBuilder.field("encryption_key", encryption_key); + } + xContentBuilder.endObject(); + return xContentBuilder; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 7b19d4f7f0..2fd3fc474d 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -114,6 +114,11 @@ public TestSecurityConfig xff(XffConfig xffConfig) { return this; } + public TestSecurityConfig onBehalfOf(OnBehalfOfConfig onBehalfOfConfig) { + config.onBehalfOfConfig(onBehalfOfConfig); + return this; + } + public TestSecurityConfig authc(AuthcDomain authcDomain) { config.authc(authcDomain); return this; @@ -170,6 +175,7 @@ public static class Config implements ToXContentObject { private Boolean doNotFailOnForbidden; private XffConfig xffConfig; + private OnBehalfOfConfig onBehalfOfConfig; private Map authcDomainMap = new LinkedHashMap<>(); private AuthFailureListeners authFailureListeners; @@ -190,6 +196,11 @@ public Config xffConfig(XffConfig xffConfig) { return this; } + public Config onBehalfOfConfig(OnBehalfOfConfig onBehalfOfConfig) { + this.onBehalfOfConfig = onBehalfOfConfig; + return this; + } + public Config authc(AuthcDomain authcDomain) { authcDomainMap.put(authcDomain.id, authcDomain); return this; @@ -210,6 +221,10 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.startObject(); xContentBuilder.startObject("dynamic"); + if (onBehalfOfConfig != null) { + xContentBuilder.field("on_behalf_of", onBehalfOfConfig); + } + if (anonymousAuth || (xffConfig != null)) { xContentBuilder.startObject("http"); xContentBuilder.field("anonymous_auth_enabled", anonymousAuth); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 539e15fb57..64207ead5b 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -55,6 +55,7 @@ import org.opensearch.test.framework.AuditConfiguration; import org.opensearch.test.framework.AuthFailureListeners; import org.opensearch.test.framework.AuthzDomain; +import org.opensearch.test.framework.OnBehalfOfConfig; import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; @@ -471,6 +472,11 @@ public Builder xff(XffConfig xffConfig) { return this; } + public Builder onBehalfOf(OnBehalfOfConfig onBehalfOfConfig) { + testSecurityConfig.onBehalfOf(onBehalfOfConfig); + return this; + } + public Builder loadConfigurationIntoIndex(boolean loadConfigurationIntoIndex) { this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; return this; diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index 4a1cb4b6aa..eff2a1db9c 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -135,6 +135,28 @@ public HttpResponse getAuthInfo(Header... headers) { return executeRequest(new HttpGet(getHttpServerUri() + "/_opendistro/_security/authinfo?pretty"), headers); } + public HttpResponse getOnBehalfOfToken(String jsonData, Header... headers) { + try { + HttpPost httpPost = new HttpPost( + new URIBuilder(getHttpServerUri() + "/_plugins/_security/api/generateonbehalfoftoken?pretty").build() + ); + httpPost.setEntity(toStringEntity(jsonData)); + return executeRequest(httpPost, mergeHeaders(CONTENT_TYPE_JSON, headers)); + } catch (URISyntaxException ex) { + throw new RuntimeException("Incorrect URI syntax", ex); + } + } + + public HttpResponse changeInternalUserPassword(String jsonData, Header... headers) { + try { + HttpPut httpPut = new HttpPut(new URIBuilder(getHttpServerUri() + "/_plugins/_security/api/account?pretty").build()); + httpPut.setEntity(toStringEntity(jsonData)); + return executeRequest(httpPut, mergeHeaders(CONTENT_TYPE_JSON, headers)); + } catch (URISyntaxException ex) { + throw new RuntimeException("Incorrect URI syntax", ex); + } + } + public void assertCorrectCredentials(String expectedUserName) { HttpResponse response = getAuthInfo(); assertThat(response, notNullValue()); diff --git a/src/integrationTest/resources/config.yml b/src/integrationTest/resources/config.yml index 5e929c0e2a..17aeb1881d 100644 --- a/src/integrationTest/resources/config.yml +++ b/src/integrationTest/resources/config.yml @@ -15,3 +15,8 @@ config: authentication_backend: type: "internal" config: {} + on_behalf_of: + # The decoded signing key is: This is the jwt signing key for an on behalf of token authentication backend for testing of extensions + # The decoded encryption key is: encryptionKey + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java index 4eee14a3fc..03e385d5c0 100644 --- a/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java @@ -13,21 +13,14 @@ import java.nio.file.Path; import java.security.AccessController; -import java.security.Key; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; import java.security.PrivilegedAction; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; import java.util.Collection; import java.util.Map.Entry; import java.util.regex.Pattern; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.JwtParserBuilder; import io.jsonwebtoken.security.WeakKeyException; import org.apache.hc.core5.http.HttpHeaders; import org.apache.logging.log4j.LogManager; @@ -43,6 +36,7 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.util.KeyUtils; public class HTTPJwtAuthenticator implements HTTPAuthenticator { @@ -63,45 +57,7 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator { public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { super(); - JwtParser _jwtParser = null; - - try { - String signingKey = settings.get("signing_key"); - - if (signingKey == null || signingKey.length() == 0) { - log.error("signingKey must not be null or empty. JWT authentication will not work"); - } else { - - signingKey = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", ""); - signingKey = signingKey.replace("-----END PUBLIC KEY-----", ""); - - byte[] decoded = Decoders.BASE64.decode(signingKey); - Key key = null; - - try { - key = getPublicKey(decoded, "RSA"); - } catch (Exception e) { - log.debug("No public RSA key, try other algos ({})", e.toString()); - } - - try { - key = getPublicKey(decoded, "EC"); - } catch (Exception e) { - log.debug("No public ECDSA key, try other algos ({})", e.toString()); - } - - if (key != null) { - _jwtParser = Jwts.parser().setSigningKey(key); - } else { - _jwtParser = Jwts.parser().setSigningKey(decoded); - } - - } - } catch (Throwable e) { - log.error("Error creating JWT authenticator. JWT authentication will not work", e); - throw new RuntimeException(e); - } - + String signingKey = settings.get("signing_key"); jwtUrlParameter = settings.get("jwt_url_parameter"); jwtHeaderName = settings.get("jwt_header", HttpHeaders.AUTHORIZATION); isDefaultAuthHeader = HttpHeaders.AUTHORIZATION.equalsIgnoreCase(jwtHeaderName); @@ -110,15 +66,20 @@ public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { requireAudience = settings.get("required_audience"); requireIssuer = settings.get("required_issuer"); - if (requireAudience != null) { - _jwtParser.requireAudience(requireAudience); - } + JwtParserBuilder jwtParserBuilder = KeyUtils.createJwtParserBuilderFromSigningKey(signingKey, log); + if (jwtParserBuilder == null) { + jwtParser = null; + } else { + if (requireAudience != null) { + jwtParserBuilder = jwtParserBuilder.require("aud", requireAudience); + } - if (requireIssuer != null) { - _jwtParser.requireIssuer(requireIssuer); - } + if (requireIssuer != null) { + jwtParserBuilder = jwtParserBuilder.require("iss", requireIssuer); + } - jwtParser = _jwtParser; + jwtParser = jwtParserBuilder.build(); + } } @Override @@ -282,11 +243,4 @@ protected String[] extractRoles(final Claims claims, final RestRequest request) return roles; } - private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, - InvalidKeySpecException { - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory kf = KeyFactory.getInstance(algo); - return kf.generatePublic(spec); - } - } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index c7c666bdaf..c34e3877f1 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -106,6 +106,7 @@ import org.opensearch.indices.IndicesService; import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.plugins.ClusterPlugin; +import org.opensearch.plugins.ExtensionAwarePlugin; import org.opensearch.plugins.MapperPlugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; @@ -118,6 +119,7 @@ import org.opensearch.search.query.QuerySearchResult; import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.TransportConfigUpdateAction; +import org.opensearch.security.action.onbehalf.CreateOnBehalfOfTokenAction; import org.opensearch.security.action.whoami.TransportWhoAmIAction; import org.opensearch.security.action.whoami.WhoAmIAction; import org.opensearch.security.auditlog.AuditLog; @@ -193,7 +195,15 @@ import org.opensearch.watcher.ResourceWatcherService; // CS-ENFORCE-SINGLE -public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin implements ClusterPlugin, MapperPlugin { +public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin + implements + ClusterPlugin, + MapperPlugin, + // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings + ExtensionAwarePlugin +// CS-ENFORCE-SINGLE + +{ private static final String KEYWORD = ".keyword"; private static final Logger actionTrace = LogManager.getLogger("opendistro_security_action_trace"); @@ -218,6 +228,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile SslExceptionHandler sslExceptionHandler; private volatile Client localClient; private final boolean disabled; + private volatile DynamicConfigFactory dcf; private final List demoCertHashes = new ArrayList(3); private volatile SecurityFilter sf; private volatile IndexResolverReplacer irr; @@ -534,6 +545,9 @@ public List getRestHandlers( principalExtractor ) ); + CreateOnBehalfOfTokenAction cobot = new CreateOnBehalfOfTokenAction(settings, threadPool, Objects.requireNonNull(cs)); + dcf.registerDCFListener(cobot); + handlers.add(cobot); handlers.addAll( SecurityRestApiActions.getHandler( settings, @@ -945,7 +959,7 @@ public Collection createComponents( // Register opensearch dynamic settings transportPassiveAuthSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); - final ClusterInfoHolder cih = new ClusterInfoHolder(); + final ClusterInfoHolder cih = new ClusterInfoHolder(this.cs.getClusterName().value()); this.cs.addListener(cih); this.salt = Salt.from(settings); @@ -1035,8 +1049,7 @@ public Collection createComponents( configPath, compatConfig ); - - final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); + dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); dcf.registerDCFListener(backendRegistry); dcf.registerDCFListener(compatConfig); dcf.registerDCFListener(irr); diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java new file mode 100644 index 0000000000..2459d469df --- /dev/null +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -0,0 +1,179 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.action.onbehalf; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableList; +import org.greenrobot.eventbus.Subscribe; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.NamedRoute; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class CreateOnBehalfOfTokenAction extends BaseRestHandler { + + private static final List routes = addRoutesPrefix( + ImmutableList.of(new NamedRoute.Builder().method(POST).path("/generateonbehalfoftoken").uniqueName("security:obo/create").build()), + "/_plugins/_security/api" + ); + + private JwtVendor vendor; + private final ThreadPool threadPool; + private final ClusterService clusterService; + + private ConfigModel configModel; + + private DynamicConfigModel dcm; + + public static final Integer OBO_DEFAULT_EXPIRY_SECONDS = 5 * 60; + public static final Integer OBO_MAX_EXPIRY_SECONDS = 10 * 60; + + public static final String DEFAULT_SERVICE = "self-issued"; + + @Subscribe + public void onConfigModelChanged(ConfigModel configModel) { + this.configModel = configModel; + } + + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + this.dcm = dcm; + + Settings settings = dcm.getDynamicOnBehalfOfSettings(); + + Boolean enabled = Boolean.parseBoolean(settings.get("enabled")); + String signingKey = settings.get("signing_key"); + String encryptionKey = settings.get("encryption_key"); + + if (!Boolean.FALSE.equals(enabled) && signingKey != null && encryptionKey != null) { + this.vendor = new JwtVendor(settings, Optional.empty()); + } else { + this.vendor = null; + } + } + + public CreateOnBehalfOfTokenAction(final Settings settings, final ThreadPool threadPool, final ClusterService clusterService) { + this.threadPool = threadPool; + this.clusterService = clusterService; + } + + @Override + public String getName() { + return getClass().getSimpleName(); + } + + @Override + public List routes() { + return routes; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + switch (request.method()) { + case POST: + return handlePost(request, client); + default: + throw new IllegalArgumentException(request.method() + " not supported"); + } + } + + private RestChannelConsumer handlePost(RestRequest request, NodeClient client) throws IOException { + return new RestChannelConsumer() { + @Override + public void accept(RestChannel channel) throws Exception { + final XContentBuilder builder = channel.newBuilder(); + BytesRestResponse response; + try { + if (vendor == null) { + channel.sendResponse( + new BytesRestResponse( + RestStatus.SERVICE_UNAVAILABLE, + "The OnBehalfOf token generating API has been disabled, see {link to doc} for more information on this feature." /* TODO: Update the link to the documentation website */ + ) + ); + return; + } + + final String clusterIdentifier = clusterService.getClusterName().value(); + + final Map requestBody = request.contentOrSourceParamParser().map(); + final String description = (String) requestBody.getOrDefault("description", null); + + final Integer tokenDuration = Optional.ofNullable(requestBody.get("durationSeconds")) + .map(value -> (String) value) + .map(Integer::parseInt) + .map(value -> Math.min(value, OBO_MAX_EXPIRY_SECONDS)) // Max duration seconds are 600 + .orElse(OBO_DEFAULT_EXPIRY_SECONDS); // Fallback to default + + final Boolean roleSecurityMode = Optional.ofNullable(requestBody.get("roleSecurityMode")) + .map(value -> (Boolean) value) + .orElse(true); // Default to false if null + + final String service = (String) requestBody.getOrDefault("service", DEFAULT_SERVICE); + final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + Set mappedRoles = mapRoles(user); + + builder.startObject(); + builder.field("user", user.getName()); + + final String token = vendor.createJwt( + clusterIdentifier, + user.getName(), + service, + tokenDuration, + mappedRoles.stream().collect(Collectors.toList()), + user.getRoles().stream().collect(Collectors.toList()), + roleSecurityMode + ); + builder.field("authenticationToken", token); + builder.field("durationSeconds", tokenDuration); + builder.endObject(); + + response = new BytesRestResponse(RestStatus.OK, builder); + } catch (final Exception exception) { + builder.startObject().field("error", exception.toString()).endObject(); + + response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); + } + builder.close(); + channel.sendResponse(response); + } + }; + } + + private Set mapRoles(final User user) { + return this.configModel.mapSecurityRoles(user, null); + } + +} diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 9721664c70..c16f90fb6a 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -59,6 +59,7 @@ import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.ssl.util.Utils; @@ -606,6 +607,12 @@ private User impersonate(final RestRequest request, final User originalUser) thr // loop over all http/rest auth domains for (final AuthDomain authDomain : restAuthDomains) { final AuthenticationBackend authenticationBackend = authDomain.getBackend(); + + // Skip over the OnBehalfOfAuthenticator since it is not compatible for user impersonation + if (authDomain.getHttpAuthenticator() instanceof OnBehalfOfAuthenticator) { + continue; + } + final User impersonatedUser = checkExistsAndAuthz( restImpersonationCache, new User(impersonatedUserHeader), diff --git a/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java b/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java index 1f149aabcf..299a1a4577 100644 --- a/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java +++ b/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java @@ -46,7 +46,9 @@ public String getType() { @Override public User authenticate(final AuthCredentials credentials) { - return new User(credentials.getUsername(), credentials.getBackendRoles(), credentials); + User user = new User(credentials.getUsername(), credentials.getBackendRoles(), credentials); + user.addSecurityRoles(credentials.getSecurityRoles()); + return user; } @Override diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java new file mode 100644 index 0000000000..2e11fed64a --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.authtoken.jwt; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +public class EncryptionDecryptionUtil { + + private final Cipher encryptCipher; + private final Cipher decryptCipher; + + public EncryptionDecryptionUtil(final String secret) { + this.encryptCipher = createCipherFromSecret(secret, CipherMode.ENCRYPT); + this.decryptCipher = createCipherFromSecret(secret, CipherMode.DECRYPT); + } + + public String encrypt(final String data) { + byte[] encryptedBytes = processWithCipher(data.getBytes(StandardCharsets.UTF_8), encryptCipher); + return Base64.getEncoder().encodeToString(encryptedBytes); + } + + public String decrypt(final String encryptedString) { + byte[] decodedBytes = Base64.getDecoder().decode(encryptedString); + return new String(processWithCipher(decodedBytes, decryptCipher), StandardCharsets.UTF_8); + } + + private static Cipher createCipherFromSecret(final String secret, final CipherMode mode) { + try { + final byte[] decodedKey = Base64.getDecoder().decode(secret); + final Cipher cipher = Cipher.getInstance("AES"); + final SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); + cipher.init(mode.opmode, originalKey); + return cipher; + } catch (final Exception e) { + throw new RuntimeException("Error creating cipher from secret in mode " + mode.name(), e); + } + } + + private static byte[] processWithCipher(final byte[] data, final Cipher cipher) { + try { + return cipher.doFinal(data); + } catch (final Exception e) { + throw new RuntimeException("Error processing data with cipher", e); + } + } + + private enum CipherMode { + ENCRYPT(Cipher.ENCRYPT_MODE), + DECRYPT(Cipher.DECRYPT_MODE); + + private final int opmode; + + private CipherMode(final int opmode) { + this.opmode = opmode; + } + } +} diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java new file mode 100644 index 0000000000..5d3262799f --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -0,0 +1,171 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.authtoken.jwt; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.function.LongSupplier; + +import com.google.common.base.Strings; +import org.apache.cxf.jaxrs.json.basic.JsonMapObjectReaderWriter; +import org.apache.cxf.rs.security.jose.jwk.JsonWebKey; +import org.apache.cxf.rs.security.jose.jwk.KeyType; +import org.apache.cxf.rs.security.jose.jwk.PublicKeyUse; +import org.apache.cxf.rs.security.jose.jws.JwsUtils; +import org.apache.cxf.rs.security.jose.jwt.JoseJwtProducer; +import org.apache.cxf.rs.security.jose.jwt.JwtClaims; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.apache.cxf.rs.security.jose.jwt.JwtUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.ssl.util.ExceptionUtils; + +public class JwtVendor { + private static final Logger logger = LogManager.getLogger(JwtVendor.class); + + private static JsonMapObjectReaderWriter jsonMapReaderWriter = new JsonMapObjectReaderWriter(); + + private final String claimsEncryptionKey; + private final JsonWebKey signingKey; + private final JoseJwtProducer jwtProducer; + private final LongSupplier timeProvider; + private final EncryptionDecryptionUtil encryptionDecryptionUtil; + private final Integer defaultExpirySeconds = 300; + private final Integer maxExpirySeconds = 600; + + public JwtVendor(final Settings settings, final Optional timeProvider) { + JoseJwtProducer jwtProducer = new JoseJwtProducer(); + try { + this.signingKey = createJwkFromSettings(settings); + } catch (Exception e) { + throw ExceptionUtils.createJwkCreationException(e); + } + this.jwtProducer = jwtProducer; + if (settings.get("encryption_key") == null) { + throw new IllegalArgumentException("encryption_key cannot be null"); + } else { + this.claimsEncryptionKey = settings.get("encryption_key"); + this.encryptionDecryptionUtil = new EncryptionDecryptionUtil(claimsEncryptionKey); + } + if (timeProvider.isPresent()) { + this.timeProvider = timeProvider.get(); + } else { + this.timeProvider = () -> System.currentTimeMillis() / 1000; + } + } + + /* + * The default configuration of this web key should be: + * KeyType: OCTET + * PublicKeyUse: SIGN + * Encryption Algorithm: HS512 + * */ + static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { + String signingKey = settings.get("signing_key"); + + if (!Strings.isNullOrEmpty(signingKey)) { + + JsonWebKey jwk = new JsonWebKey(); + + jwk.setKeyType(KeyType.OCTET); + jwk.setAlgorithm("HS512"); + jwk.setPublicKeyUse(PublicKeyUse.SIGN); + jwk.setProperty("k", signingKey); + + return jwk; + } else { + Settings jwkSettings = settings.getAsSettings("jwt").getAsSettings("key"); + + if (jwkSettings.isEmpty()) { + throw new Exception( + "Settings for signing key is missing. Please specify at least the option signing_key with a shared secret." + ); + } + + JsonWebKey jwk = new JsonWebKey(); + + for (String key : jwkSettings.keySet()) { + jwk.setProperty(key, jwkSettings.get(key)); + } + + return jwk; + } + } + + public String createJwt( + String issuer, + String subject, + String audience, + Integer expirySeconds, + List roles, + List backendRoles, + boolean roleSecurityMode + ) throws Exception { + final long nowAsMillis = timeProvider.getAsLong(); + final Instant nowAsInstant = Instant.ofEpochMilli(timeProvider.getAsLong()); + + jwtProducer.setSignatureProvider(JwsUtils.getSignatureProvider(signingKey)); + JwtClaims jwtClaims = new JwtClaims(); + JwtToken jwt = new JwtToken(jwtClaims); + + jwtClaims.setIssuer(issuer); + + jwtClaims.setIssuedAt(nowAsMillis); + + jwtClaims.setSubject(subject); + + jwtClaims.setAudience(audience); + + jwtClaims.setNotBefore(nowAsMillis); + + if (expirySeconds > maxExpirySeconds) { + throw new Exception("The provided expiration time exceeds the maximum allowed duration of " + maxExpirySeconds + " seconds"); + } + + expirySeconds = (expirySeconds == null) ? defaultExpirySeconds : Math.min(expirySeconds, maxExpirySeconds); + if (expirySeconds <= 0) { + throw new Exception("The expiration time should be a positive integer"); + } + long expiryTime = timeProvider.getAsLong() + expirySeconds; + jwtClaims.setExpiryTime(expiryTime); + + if (roles != null) { + String listOfRoles = String.join(",", roles); + jwtClaims.setProperty("er", encryptionDecryptionUtil.encrypt(listOfRoles)); + } else { + throw new Exception("Roles cannot be null"); + } + + if (!roleSecurityMode && backendRoles != null) { + String listOfBackendRoles = String.join(",", backendRoles); + jwtClaims.setProperty("br", listOfBackendRoles); + } + + String encodedJwt = jwtProducer.processJwt(jwt); + + if (logger.isDebugEnabled()) { + logger.debug( + "Created JWT: " + + encodedJwt + + "\n" + + jsonMapReaderWriter.toJson(jwt.getJwsHeaders()) + + "\n" + + JwtUtils.claimsToJson(jwt.getClaims()) + ); + } + + return encodedJwt; + } +} diff --git a/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java b/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java index 00101d9a73..d7429c5d1d 100644 --- a/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java +++ b/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java @@ -40,6 +40,11 @@ public class ClusterInfoHolder implements ClusterStateListener { private volatile DiscoveryNodes nodes = null; private volatile Boolean isLocalNodeElectedClusterManager = null; private volatile boolean initialized; + private final String clusterName; + + public ClusterInfoHolder(String clusterName) { + this.clusterName = clusterName; + } @Override public void clusterChanged(ClusterChangedEvent event) { @@ -72,4 +77,8 @@ public Boolean hasNode(DiscoveryNode node) { return nodes.nodeExists(node) ? Boolean.TRUE : Boolean.FALSE; } + + public String getClusterName() { + return this.clusterName; + } } diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java new file mode 100644 index 0000000000..467edd8ac4 --- /dev/null +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -0,0 +1,255 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.http; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.List; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.JwtParserBuilder; +import io.jsonwebtoken.security.WeakKeyException; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.SpecialPermission; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.auth.HTTPAuthenticator; +import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; +import org.opensearch.security.ssl.util.ExceptionUtils; +import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.util.KeyUtils; + +import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; + +public class OnBehalfOfAuthenticator implements HTTPAuthenticator { + + private static final String REGEX_PATH_PREFIX = "/(" + LEGACY_OPENDISTRO_PREFIX + "|" + PLUGINS_PREFIX + ")/" + "(.*)"; + private static final Pattern PATTERN_PATH_PREFIX = Pattern.compile(REGEX_PATH_PREFIX); + private static final String ON_BEHALF_OF_SUFFIX = "api/generateonbehalfoftoken"; + private static final String ACCOUNT_SUFFIX = "api/account"; + + protected final Logger log = LogManager.getLogger(this.getClass()); + + private static final Pattern BEARER = Pattern.compile("^\\s*Bearer\\s.*", Pattern.CASE_INSENSITIVE); + private static final String BEARER_PREFIX = "bearer "; + + private final JwtParser jwtParser; + private final String encryptionKey; + private final Boolean oboEnabled; + private final String clusterName; + + private final EncryptionDecryptionUtil encryptionUtil; + + public OnBehalfOfAuthenticator(Settings settings, String clusterName) { + String oboEnabledSetting = settings.get("enabled", "true"); + oboEnabled = Boolean.parseBoolean(oboEnabledSetting); + encryptionKey = settings.get("encryption_key"); + JwtParserBuilder builder = initParserBuilder(settings.get("signing_key")); + jwtParser = builder.build(); + + this.clusterName = clusterName; + this.encryptionUtil = new EncryptionDecryptionUtil(encryptionKey); + } + + private JwtParserBuilder initParserBuilder(final String signingKey) { + JwtParserBuilder jwtParserBuilder = KeyUtils.createJwtParserBuilderFromSigningKey(signingKey, log); + + if (jwtParserBuilder == null) { + throw new OpenSearchSecurityException("Unable to find on behalf of authenticator signing key"); + } + + return jwtParserBuilder; + } + + private List extractSecurityRolesFromClaims(Claims claims) { + Object er = claims.get("er"); + Object dr = claims.get("dr"); + String rolesClaim = ""; + + if (er != null) { + rolesClaim = encryptionUtil.decrypt(er.toString()); + } else if (dr != null) { + rolesClaim = dr.toString(); + } else { + log.warn("This is a malformed On-behalf-of Token"); + } + + List roles = Arrays.stream(rolesClaim.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toUnmodifiableList()); + + return roles; + } + + private String[] extractBackendRolesFromClaims(Claims claims) { + Object backendRolesObject = claims.get("br"); + String[] backendRoles; + + if (backendRolesObject == null) { + backendRoles = new String[0]; + } else { + // Extracting roles based on the compatibility mode + backendRoles = Arrays.stream(backendRolesObject.toString().split(",")).map(String::trim).toArray(String[]::new); + } + + return backendRoles; + } + + @Override + @SuppressWarnings("removal") + public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException { + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + AuthCredentials creds = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public AuthCredentials run() { + return extractCredentials0(request); + } + }); + + return creds; + } + + private AuthCredentials extractCredentials0(final RestRequest request) { + if (!oboEnabled) { + log.error("On-behalf-of authentication is disabled"); + return null; + } + + if (jwtParser == null) { + log.error("Missing Signing Key. JWT authentication will not work"); + return null; + } + + String jwtToken = extractJwtFromHeader(request); + if (jwtToken == null) { + return null; + } + + if (!isRequestAllowed(request)) { + return null; + } + + try { + final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); + + final String subject = claims.getSubject(); + if (subject == null) { + log.error("Valid jwt on behalf of token with no subject"); + return null; + } + + final String audience = claims.getAudience(); + if (audience == null) { + log.error("Valid jwt on behalf of token with no audience"); + return null; + } + + final String issuer = claims.getIssuer(); + if (!clusterName.equals(issuer)) { + log.error("The issuer of this OBO does not match the current cluster identifier"); + return null; + } + + List roles = extractSecurityRolesFromClaims(claims); + String[] backendRoles = extractBackendRolesFromClaims(claims); + + final AuthCredentials ac = new AuthCredentials(subject, roles, backendRoles).markComplete(); + + for (Entry claim : claims.entrySet()) { + ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); + } + + return ac; + + } catch (WeakKeyException e) { + log.error("Cannot authenticate user with JWT because of ", e); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Invalid or expired JWT token.", e); + } + } + + // Return null for the authentication failure + return null; + } + + private String extractJwtFromHeader(RestRequest request) { + String jwtToken = request.header(HttpHeaders.AUTHORIZATION); + + if (jwtToken == null || jwtToken.isEmpty()) { + logDebug("No JWT token found in '{}' header", HttpHeaders.AUTHORIZATION); + return null; + } + + if (!BEARER.matcher(jwtToken).matches()) { + return null; + } + + if (jwtToken.toLowerCase().contains(BEARER_PREFIX)) { + jwtToken = jwtToken.substring(jwtToken.toLowerCase().indexOf(BEARER_PREFIX) + BEARER_PREFIX.length()); + } else { + logDebug("No Bearer scheme found in header"); + return null; + } + + return jwtToken; + } + + private void logDebug(String message, Object... args) { + if (log.isDebugEnabled()) { + log.debug(message, args); + } + } + + public Boolean isRequestAllowed(final RestRequest request) { + Matcher matcher = PATTERN_PATH_PREFIX.matcher(request.path()); + final String suffix = matcher.matches() ? matcher.group(2) : null; + if (request.method() == RestRequest.Method.POST && ON_BEHALF_OF_SUFFIX.equals(suffix) + || request.method() == RestRequest.Method.PUT && ACCOUNT_SUFFIX.equals(suffix)) { + final OpenSearchException exception = ExceptionUtils.invalidUsageOfOBOTokenException(); + log.error(exception.toString()); + return false; + } + return true; + } + + @Override + public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { + return false; + } + + @Override + public String getType() { + return "onbehalfof_jwt"; + } + +} diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index 781b8a626c..bcbe3aef57 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -128,6 +128,7 @@ public final static SecurityDynamicConfiguration addStatics(SecurityDynamicCo private final Settings opensearchSettings; private final Path configPath; private final InternalAuthenticationBackend iab = new InternalAuthenticationBackend(); + private final ClusterInfoHolder cih; SecurityDynamicConfiguration config; @@ -143,6 +144,7 @@ public DynamicConfigFactory( this.cr = cr; this.opensearchSettings = opensearchSettings; this.configPath = configPath; + this.cih = cih; if (opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES, true)) { try { @@ -269,7 +271,7 @@ public void onChange(Map> typeToConfig) { ); // rebuild v7 Models - dcm = new DynamicConfigModelV7(getConfigV7(config), opensearchSettings, configPath, iab); + dcm = new DynamicConfigModelV7(getConfigV7(config), opensearchSettings, configPath, iab, this.cih); ium = new InternalUsersModelV7( (SecurityDynamicConfiguration) internalusers, (SecurityDynamicConfiguration) roles, diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java index 08976f2013..e3d10878da 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java @@ -38,6 +38,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.common.settings.Settings; import org.opensearch.security.auth.AuthDomain; import org.opensearch.security.auth.AuthFailureListener; import org.opensearch.security.auth.AuthorizationBackend; @@ -104,6 +105,8 @@ public abstract class DynamicConfigModel { public abstract Multimap> getAuthBackendClientBlockRegistries(); + public abstract Settings getDynamicOnBehalfOfSettings(); + protected final Map authImplMap = new HashMap<>(); public DynamicConfigModel() { diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java index 994989416b..e5308aa574 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java @@ -207,6 +207,11 @@ public Multimap> getAuthBackendClientBlockRe return Multimaps.unmodifiableMultimap(authBackendClientBlockRegistries); } + @Override + public Settings getDynamicOnBehalfOfSettings() { + return Settings.EMPTY; + } + private void buildAAA() { final SortedSet restAuthDomains0 = new TreeSet<>(); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 60637e4b8c..fcbf985f60 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -57,6 +57,9 @@ import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.InternalAuthenticationBackend; +import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; +import org.opensearch.security.configuration.ClusterInfoHolder; +import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.securityconf.impl.v7.ConfigV7; import org.opensearch.security.securityconf.impl.v7.ConfigV7.Authc; import org.opensearch.security.securityconf.impl.v7.ConfigV7.AuthcDomain; @@ -80,13 +83,21 @@ public class DynamicConfigModelV7 extends DynamicConfigModel { private Multimap authBackendFailureListeners; private List> ipClientBlockRegistries; private Multimap> authBackendClientBlockRegistries; - - public DynamicConfigModelV7(ConfigV7 config, Settings opensearchSettings, Path configPath, InternalAuthenticationBackend iab) { + private final ClusterInfoHolder cih; + + public DynamicConfigModelV7( + ConfigV7 config, + Settings opensearchSettings, + Path configPath, + InternalAuthenticationBackend iab, + ClusterInfoHolder cih + ) { super(); this.config = config; this.opensearchSettings = opensearchSettings; this.configPath = configPath; this.iab = iab; + this.cih = cih; buildAAA(); } @@ -210,6 +221,13 @@ public Multimap> getAuthBackendClientBlockRe return Multimaps.unmodifiableMultimap(authBackendClientBlockRegistries); } + @Override + public Settings getDynamicOnBehalfOfSettings() { + return Settings.builder() + .put(Settings.builder().loadFromSource(config.dynamic.on_behalf_of.configAsJson(), XContentType.JSON).build()) + .build(); + } + private void buildAAA() { final SortedSet restAuthDomains0 = new TreeSet<>(); @@ -358,6 +376,23 @@ private void buildAAA() { } } + /* + * If the OnBehalfOf (OBO) authentication is configured: + * Add the OBO authbackend in to the auth domains + * Challenge: false - no need to iterate through the auth domains again when OBO authentication failed + * order: -1 - prioritize the OBO authentication when it gets enabled + */ + Settings oboSettings = getDynamicOnBehalfOfSettings(); + if (oboSettings.get("signing_key") != null && oboSettings.get("encryption_key") != null) { + final AuthDomain _ad = new AuthDomain( + new NoOpAuthenticationBackend(Settings.EMPTY, null), + new OnBehalfOfAuthenticator(getDynamicOnBehalfOfSettings(), this.cih.getClusterName()), + false, + -1 + ); + restAuthDomains0.add(_ad); + } + List originalDestroyableComponents = destroyableComponents; restAuthDomains = Collections.unmodifiableSortedSet(restAuthDomains0); diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java index c85e69fb0d..1e92c5948a 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java @@ -36,6 +36,7 @@ import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import org.opensearch.security.DefaultObjectMapper; @@ -356,4 +357,51 @@ public String toString() { } + public static class OnBehalfOfSettings { + @JsonProperty("enabled") + private Boolean oboEnabled = Boolean.TRUE; + @JsonProperty("signing_key") + private String signingKey; + @JsonProperty("encryption_key") + private String encryptionKey; + + @JsonIgnore + public String configAsJson() { + try { + return DefaultObjectMapper.writeValueAsString(this, false); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public Boolean getOboEnabled() { + return oboEnabled; + } + + public void setOboEnabled(Boolean oboEnabled) { + this.oboEnabled = oboEnabled; + } + + public String getSigningKey() { + return signingKey; + } + + public void setSigningKey(String signingKey) { + this.signingKey = signingKey; + } + + public String getEncryptionKey() { + return encryptionKey; + } + + public void setEncryptionKey(String encryptionKey) { + this.encryptionKey = encryptionKey; + } + + @Override + public String toString() { + return "OnBehalfOf [ enabled=" + oboEnabled + ", signing_key=" + signingKey + ", encryption_key=" + encryptionKey + "]"; + } + } + } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java index 87de6a31b0..49fa3a13cc 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java @@ -37,6 +37,7 @@ import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import org.opensearch.security.DefaultObjectMapper; @@ -133,6 +134,7 @@ public static class Dynamic { public String hosts_resolver_mode = "ip-only"; public String transport_userrname_attribute; public boolean do_not_fail_on_forbidden_empty; + public OnBehalfOfSettings on_behalf_of = new OnBehalfOfSettings(); @Override public String toString() { @@ -478,4 +480,51 @@ public String toString() { } + public static class OnBehalfOfSettings { + @JsonProperty("enabled") + private Boolean oboEnabled = Boolean.TRUE; + @JsonProperty("signing_key") + private String signingKey; + @JsonProperty("encryption_key") + private String encryptionKey; + + @JsonIgnore + public String configAsJson() { + try { + return DefaultObjectMapper.writeValueAsString(this, false); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public Boolean getOboEnabled() { + return oboEnabled; + } + + public void setOboEnabled(Boolean oboEnabled) { + this.oboEnabled = oboEnabled; + } + + public String getSigningKey() { + return signingKey; + } + + public void setSigningKey(String signingKey) { + this.signingKey = signingKey; + } + + public String getEncryptionKey() { + return encryptionKey; + } + + public void setEncryptionKey(String encryptionKey) { + this.encryptionKey = encryptionKey; + } + + @Override + public String toString() { + return "OnBehalfOf [ enabled=" + oboEnabled + ", signing_key=" + signingKey + ", encryption_key=" + encryptionKey + "]"; + } + } + } diff --git a/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java b/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java index 9d6d3dade8..83982239f0 100644 --- a/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java +++ b/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java @@ -64,6 +64,18 @@ public static OpenSearchException createBadHeaderException() { ); } + public static OpenSearchException invalidUsageOfOBOTokenException() { + return new OpenSearchException("On-Behalf-Of Token is not allowed to be used for accessing this endpoint."); + } + + public static OpenSearchException createJwkCreationException() { + return new OpenSearchException("An error occurred during the creation of Jwk."); + } + + public static OpenSearchException createJwkCreationException(Throwable cause) { + return new OpenSearchException("An error occurred during the creation of Jwk: {}", cause, cause.getMessage()); + } + public static OpenSearchException createTransportClientNoLongerSupportedException() { return new OpenSearchException("Transport client authentication no longer supported."); } diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index ee04ff62f3..61962a61f7 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -320,6 +320,12 @@ public enum RolesMappingResolution { public static final String TENANCY_GLOBAL_TENANT_NAME = "global"; public static final String TENANCY_GLOBAL_TENANT_DEFAULT_NAME = ""; + // On-behalf-of endpoints settings + // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings + public static final String EXTENSIONS_BWC_PLUGIN_MODE = "bwcPluginMode"; + public static final boolean EXTENSIONS_BWC_PLUGIN_MODE_DEFAULT = false; + // CS-ENFORCE-SINGLE + public static Set getSettingAsSet( final Settings settings, final String key, diff --git a/src/main/java/org/opensearch/security/user/AuthCredentials.java b/src/main/java/org/opensearch/security/user/AuthCredentials.java index cab3eab6fd..beb3ae1733 100644 --- a/src/main/java/org/opensearch/security/user/AuthCredentials.java +++ b/src/main/java/org/opensearch/security/user/AuthCredentials.java @@ -32,6 +32,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -48,6 +49,7 @@ public final class AuthCredentials { private final String username; private byte[] password; private Object nativeCredentials; + private final Set securityRoles = new HashSet(); private final Set backendRoles = new HashSet(); private boolean complete; private final byte[] internalPasswordHash; @@ -94,6 +96,18 @@ public AuthCredentials(final String username, String... backendRoles) { this(username, null, null, backendRoles); } + /** + * Create new credentials with a username, a initial optional set of roles and empty password/native credentials + * @param username The username, must not be null or empty + * @param securityRoles The internal roles the user has been mapped to + * @param backendRoles set of roles this user is a member of + * @throws IllegalArgumentException if username is null or empty + */ + public AuthCredentials(final String username, List securityRoles, String... backendRoles) { + this(username, null, null, backendRoles); + this.securityRoles.addAll(securityRoles); + } + private AuthCredentials(final String username, byte[] password, Object nativeCredentials, String... backendRoles) { super(); @@ -203,6 +217,14 @@ public Set getBackendRoles() { return new HashSet(backendRoles); } + /** + * + * @return Defensive copy of the security roles this user is member of. + */ + public Set getSecurityRoles() { + return Set.copyOf(securityRoles); + } + public boolean isComplete() { return complete; } diff --git a/src/main/java/org/opensearch/security/util/KeyUtils.java b/src/main/java/org/opensearch/security/util/KeyUtils.java new file mode 100644 index 0000000000..4aebf0cb12 --- /dev/null +++ b/src/main/java/org/opensearch/security/util/KeyUtils.java @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.util; + +import io.jsonwebtoken.JwtParserBuilder; +import io.jsonwebtoken.Jwts; +import org.apache.logging.log4j.Logger; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.SpecialPermission; +import org.opensearch.core.common.Strings; + +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Objects; + +public class KeyUtils { + + public static JwtParserBuilder createJwtParserBuilderFromSigningKey(final String signingKey, final Logger log) { + final SecurityManager sm = System.getSecurityManager(); + + JwtParserBuilder jwtParserBuilder = null; + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + jwtParserBuilder = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public JwtParserBuilder run() { + if (Strings.isNullOrEmpty(signingKey)) { + log.error("Unable to find signing key"); + return null; + } else { + try { + Key key = null; + + final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "") + .replace("-----END PUBLIC KEY-----", ""); + + final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); + + try { + key = getPublicKey(decoded, "RSA"); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + log.debug("No public RSA key, try other algos ({})", e.toString()); + } + + try { + key = getPublicKey(decoded, "EC"); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + log.debug("No public ECDSA key, try other algos ({})", e.toString()); + } + + if (Objects.nonNull(key)) { + return Jwts.parserBuilder().setSigningKey(key); + } + + return Jwts.parserBuilder().setSigningKey(decoded); + } catch (Throwable e) { + log.error("Error while creating JWT authenticator", e); + throw new OpenSearchSecurityException(e.toString(), e); + } + } + } + }); + + return jwtParserBuilder; + } + + private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, + InvalidKeySpecException { + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance(algo); + return kf.generatePublic(spec); + } + +} diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtilsTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtilsTest.java new file mode 100644 index 0000000000..4890f380f9 --- /dev/null +++ b/src/test/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtilsTest.java @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.authtoken.jwt; + +import org.junit.Assert; +import org.junit.Test; +import java.util.Base64; + +public class EncryptionDecryptionUtilsTest { + + @Test + public void testEncryptDecrypt() { + String secret = Base64.getEncoder().encodeToString("mySecretKey12345".getBytes()); + String data = "Hello, OpenSearch!"; + + EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(secret); + + String encryptedString = util.encrypt(data); + String decryptedString = util.decrypt(encryptedString); + + Assert.assertEquals(data, decryptedString); + } + + @Test + public void testDecryptingWithWrongKey() { + String secret1 = Base64.getEncoder().encodeToString("correctKey12345".getBytes()); + String secret2 = Base64.getEncoder().encodeToString("wrongKey1234567".getBytes()); + String data = "Hello, OpenSearch!"; + + EncryptionDecryptionUtil util1 = new EncryptionDecryptionUtil(secret1); + String encryptedString = util1.encrypt(data); + + EncryptionDecryptionUtil util2 = new EncryptionDecryptionUtil(secret2); + RuntimeException ex = Assert.assertThrows(RuntimeException.class, () -> util2.decrypt(encryptedString)); + + Assert.assertEquals("Error processing data with cipher", ex.getMessage()); + } + + @Test + public void testDecryptingCorruptedData() { + String secret = Base64.getEncoder().encodeToString("mySecretKey12345".getBytes()); + String corruptedEncryptedString = "corruptedData"; + + EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(secret); + RuntimeException ex = Assert.assertThrows(RuntimeException.class, () -> util.decrypt(corruptedEncryptedString)); + + Assert.assertEquals("Last unit does not have enough valid bits", ex.getMessage()); + } + + @Test + public void testEncryptDecryptEmptyString() { + String secret = Base64.getEncoder().encodeToString("mySecretKey12345".getBytes()); + String data = ""; + + EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(secret); + String encryptedString = util.encrypt(data); + String decryptedString = util.decrypt(encryptedString); + + Assert.assertEquals(data, decryptedString); + } + + @Test(expected = NullPointerException.class) + public void testEncryptNullValue() { + String secret = Base64.getEncoder().encodeToString("mySecretKey12345".getBytes()); + String data = null; + + EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(secret); + util.encrypt(data); + } + + @Test(expected = NullPointerException.class) + public void testDecryptNullValue() { + String secret = Base64.getEncoder().encodeToString("mySecretKey12345".getBytes()); + String data = null; + + EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(secret); + util.decrypt(data); + } +} diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java new file mode 100644 index 0000000000..1322777cac --- /dev/null +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -0,0 +1,185 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.authtoken.jwt; + +import java.util.List; +import java.util.Optional; +import java.util.function.LongSupplier; + +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.cxf.rs.security.jose.jwk.JsonWebKey; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.junit.Assert; +import org.junit.Test; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.support.ConfigConstants; + +public class JwtVendorTest { + + @Test + public void testCreateJwkFromSettings() throws Exception { + Settings settings = Settings.builder().put("signing_key", "abc123").build(); + + JsonWebKey jwk = JwtVendor.createJwkFromSettings(settings); + Assert.assertEquals("HS512", jwk.getAlgorithm()); + Assert.assertEquals("sig", jwk.getPublicKeyUse().toString()); + Assert.assertEquals("abc123", jwk.getProperty("k")); + } + + @Test + public void testCreateJwkFromSettingsWithoutSigningKey() { + Settings settings = Settings.builder().put("jwt", "").build(); + Throwable exception = Assert.assertThrows(RuntimeException.class, () -> { + try { + JwtVendor.createJwkFromSettings(settings); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + Assert.assertEquals( + "java.lang.Exception: Settings for signing key is missing. Please specify at least the option signing_key with a shared secret.", + exception.getMessage() + ); + } + + @Test + public void testCreateJwtWithRoles() throws Exception { + String issuer = "cluster_0"; + String subject = "admin"; + String audience = "audience_0"; + List roles = List.of("IT", "HR"); + List backendRoles = List.of("Sales", "Support"); + String expectedRoles = "IT,HR"; + int expirySeconds = 300; + LongSupplier currentTime = () -> (long) 100; + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); + Long expectedExp = currentTime.getAsLong() + expirySeconds; + + JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); + + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); + JwtToken jwt = jwtConsumer.getJwtToken(); + + Assert.assertEquals("cluster_0", jwt.getClaim("iss")); + Assert.assertEquals("admin", jwt.getClaim("sub")); + Assert.assertEquals("audience_0", jwt.getClaim("aud")); + Assert.assertNotNull(jwt.getClaim("iat")); + Assert.assertNotNull(jwt.getClaim("exp")); + Assert.assertEquals(expectedExp, jwt.getClaim("exp")); + EncryptionDecryptionUtil encryptionUtil = new EncryptionDecryptionUtil(claimsEncryptionKey); + Assert.assertEquals(expectedRoles, encryptionUtil.decrypt(jwt.getClaim("er").toString())); + Assert.assertNull(jwt.getClaim("br")); + } + + @Test + public void testCreateJwtWithRoleSecurityMode() throws Exception { + String issuer = "cluster_0"; + String subject = "admin"; + String audience = "audience_0"; + List roles = List.of("IT", "HR"); + List backendRoles = List.of("Sales", "Support"); + String expectedRoles = "IT,HR"; + String expectedBackendRoles = "Sales,Support"; + + int expirySeconds = 300; + LongSupplier currentTime = () -> (long) 100; + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + Settings settings = Settings.builder() + .put("signing_key", "abc123") + .put("encryption_key", claimsEncryptionKey) + .put(ConfigConstants.EXTENSIONS_BWC_PLUGIN_MODE, true) + .build(); + Long expectedExp = currentTime.getAsLong() + expirySeconds; + + JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); + + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); + JwtToken jwt = jwtConsumer.getJwtToken(); + + Assert.assertEquals("cluster_0", jwt.getClaim("iss")); + Assert.assertEquals("admin", jwt.getClaim("sub")); + Assert.assertEquals("audience_0", jwt.getClaim("aud")); + Assert.assertNotNull(jwt.getClaim("iat")); + Assert.assertNotNull(jwt.getClaim("exp")); + Assert.assertEquals(expectedExp, jwt.getClaim("exp")); + EncryptionDecryptionUtil encryptionUtil = new EncryptionDecryptionUtil(claimsEncryptionKey); + Assert.assertEquals(expectedRoles, encryptionUtil.decrypt(jwt.getClaim("er").toString())); + Assert.assertNotNull(jwt.getClaim("br")); + Assert.assertEquals(expectedBackendRoles, jwt.getClaim("br")); + } + + @Test + public void testCreateJwtWithBadExpiry() { + String issuer = "cluster_0"; + String subject = "admin"; + String audience = "audience_0"; + List roles = List.of("admin"); + Integer expirySeconds = -300; + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); + + Throwable exception = Assert.assertThrows(RuntimeException.class, () -> { + try { + jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + Assert.assertEquals("java.lang.Exception: The expiration time should be a positive integer", exception.getMessage()); + } + + @Test + public void testCreateJwtWithBadEncryptionKey() { + String issuer = "cluster_0"; + String subject = "admin"; + String audience = "audience_0"; + List roles = List.of("admin"); + Integer expirySeconds = 300; + + Settings settings = Settings.builder().put("signing_key", "abc123").build(); + + Throwable exception = Assert.assertThrows(RuntimeException.class, () -> { + try { + new JwtVendor(settings, Optional.empty()).createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + Assert.assertEquals("java.lang.IllegalArgumentException: encryption_key cannot be null", exception.getMessage()); + } + + @Test + public void testCreateJwtWithBadRoles() { + String issuer = "cluster_0"; + String subject = "admin"; + String audience = "audience_0"; + List roles = null; + Integer expirySeconds = 300; + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); + + Throwable exception = Assert.assertThrows(RuntimeException.class, () -> { + try { + jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + Assert.assertEquals("java.lang.Exception: Roles cannot be null", exception.getMessage()); + } +} diff --git a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java new file mode 100644 index 0000000000..fbb03bf7c3 --- /dev/null +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -0,0 +1,400 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.http; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.SecretKey; + +import com.google.common.io.BaseEncoding; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.hc.core5.http.HttpHeaders; +import org.junit.Assert; +import org.junit.Test; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.util.FakeRestRequest; + +import static org.hamcrest.Matchers.equalTo; + +public class OnBehalfOfAuthenticatorTest { + final static String clusterName = "cluster_0"; + final static String enableOBO = "true"; + final static String disableOBO = "false"; + final static String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + + final static String signingKey = + "This is my super safe signing key that no one will ever be able to guess. It's would take billions of years and the world's most powerful quantum computer to crack"; + final static String signingKeyB64Encoded = BaseEncoding.base64().encode(signingKey.getBytes(StandardCharsets.UTF_8)); + final static SecretKey secretKey = Keys.hmacShaKeyFor(signingKeyB64Encoded.getBytes(StandardCharsets.UTF_8)); + + @Test + public void testNoKey() { + Exception exception = Assert.assertThrows( + RuntimeException.class, + () -> extractCredentialsFromJwtHeader( + null, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy"), + false + ) + ); + Assert.assertTrue(exception.getMessage().contains("Unable to find on behalf of authenticator signing key")); + } + + @Test + public void testEmptyKey() { + Exception exception = Assert.assertThrows( + RuntimeException.class, + () -> extractCredentialsFromJwtHeader( + null, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy"), + false + ) + ); + Assert.assertTrue(exception.getMessage().contains("Unable to find on behalf of authenticator signing key")); + } + + @Test + public void testBadKey() { + Exception exception = Assert.assertThrows( + RuntimeException.class, + () -> extractCredentialsFromJwtHeader( + BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy"), + false + ) + ); + Assert.assertTrue(exception.getMessage().contains("The specified key byte array is 80 bits")); + } + + @Test + public void testTokenMissing() throws Exception { + + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + Map headers = new HashMap(); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + + Assert.assertNull(credentials); + } + + @Test + public void testInvalid() throws Exception { + + String jwsToken = "123invalidtoken.."; + + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + Map headers = new HashMap(); + headers.put("Authorization", "Bearer " + jwsToken); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + Assert.assertNull(credentials); + } + + @Test + public void testDisabled() throws Exception { + String jwsToken = Jwts.builder() + .setIssuer(clusterName) + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .compact(); + + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(disableOBOSettings(), clusterName); + Map headers = new HashMap(); + headers.put("Authorization", "Bearer " + jwsToken); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + Assert.assertNull(credentials); + } + + @Test + public void testNonSpecifyOBOSetting() throws Exception { + String jwsToken = Jwts.builder() + .setIssuer(clusterName) + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .compact(); + + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(nonSpecifyOBOSetting(), clusterName); + Map headers = new HashMap(); + headers.put("Authorization", "Bearer " + jwsToken); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + Assert.assertNotNull(credentials); + } + + @Test + public void testBearer() throws Exception { + Map expectedAttributes = new HashMap<>(); + expectedAttributes.put("attr.jwt.iss", "cluster_0"); + expectedAttributes.put("attr.jwt.sub", "Leonard McCoy"); + expectedAttributes.put("attr.jwt.aud", "ext_0"); + + String jwsToken = Jwts.builder() + .setIssuer(clusterName) + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .compact(); + + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + Map headers = new HashMap(); + headers.put("Authorization", "Bearer " + jwsToken); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(0, credentials.getSecurityRoles().size()); + Assert.assertEquals(0, credentials.getBackendRoles().size()); + Assert.assertThat(credentials.getAttributes(), equalTo(expectedAttributes)); + } + + @Test + public void testBearerWrongPosition() throws Exception { + + String jwsToken = Jwts.builder() + .setIssuer(clusterName) + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact(); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + + Map headers = new HashMap(); + headers.put("Authorization", jwsToken + "Bearer " + " 123"); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + + Assert.assertNull(credentials); + } + + @Test + public void testBasicAuthHeader() throws Exception { + String jwsToken = Jwts.builder() + .setIssuer(clusterName) + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact(); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + + Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + jwsToken); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, Collections.emptyMap()), null); + Assert.assertNull(credentials); + } + + @Test + public void testRoles() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy").claim("dr", "role1,role2").setAudience("svc1"), + true + ); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(2, credentials.getSecurityRoles().size()); + Assert.assertEquals(0, credentials.getBackendRoles().size()); + } + + @Test + public void testNullClaim() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy").claim("dr", null).setAudience("svc1"), + false + ); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(0, credentials.getBackendRoles().size()); + } + + @Test + public void testNonStringClaim() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy").claim("dr", 123L).setAudience("svc1"), + true + ); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(1, credentials.getSecurityRoles().size()); + Assert.assertTrue(credentials.getSecurityRoles().contains("123")); + } + + @Test + public void testRolesMissing() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy").setAudience("svc1"), + false + ); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(0, credentials.getSecurityRoles().size()); + Assert.assertEquals(0, credentials.getBackendRoles().size()); + } + + @Test + public void testWrongSubjectKey() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).claim("roles", "role1,role2").claim("asub", "Dr. Who").setAudience("svc1"), + false + ); + + Assert.assertNull(credentials); + } + + @Test + public void testExp() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Expired").setExpiration(new Date(100)), + false + ); + + Assert.assertNull(credentials); + } + + @Test + public void testNbf() throws Exception { + + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setIssuer(clusterName).setSubject("Expired").setNotBefore(new Date(System.currentTimeMillis() + (1000 * 36000))), + false + ); + + Assert.assertNull(credentials); + } + + @Test + public void testRolesArray() throws Exception { + + JwtBuilder builder = Jwts.builder() + .setPayload( + "{" + + "\"iss\": \"cluster_0\"," + + "\"typ\": \"obo\"," + + "\"sub\": \"Cluster_0\"," + + "\"aud\": \"ext_0\"," + + "\"dr\": \"a,b,3rd\"" + + "}" + ); + + final AuthCredentials credentials = extractCredentialsFromJwtHeader(signingKeyB64Encoded, claimsEncryptionKey, builder, true); + + Assert.assertNotNull(credentials); + Assert.assertEquals("Cluster_0", credentials.getUsername()); + Assert.assertEquals(3, credentials.getSecurityRoles().size()); + Assert.assertTrue(credentials.getSecurityRoles().contains("a")); + Assert.assertTrue(credentials.getSecurityRoles().contains("b")); + Assert.assertTrue(credentials.getSecurityRoles().contains("3rd")); + } + + @Test + public void testDifferentIssuer() throws Exception { + + String jwsToken = Jwts.builder() + .setIssuer("Wrong Cluster Identifier") + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .compact(); + + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + Map headers = new HashMap(); + headers.put("Authorization", "Bearer " + jwsToken); + + AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); + + Assert.assertNull(credentials); + } + + /** extracts a default user credential from a request header */ + private AuthCredentials extractCredentialsFromJwtHeader( + final String signingKeyB64Encoded, + final String encryptionKey, + final JwtBuilder jwtBuilder, + final Boolean bwcPluginCompatibilityMode + ) { + final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator( + Settings.builder() + .put("enabled", enableOBO) + .put("signing_key", signingKeyB64Encoded) + .put("encryption_key", encryptionKey) + .build(), + clusterName + ); + + final String jwsToken = jwtBuilder.signWith( + Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), + SignatureAlgorithm.HS512 + ).compact(); + final Map headers = Map.of("Authorization", "Bearer " + jwsToken); + return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()), null); + } + + private Settings defaultSettings() { + return Settings.builder() + .put("enabled", enableOBO) + .put("signing_key", signingKeyB64Encoded) + .put("encryption_key", claimsEncryptionKey) + .build(); + } + + private Settings disableOBOSettings() { + return Settings.builder() + .put("enabled", disableOBO) + .put("signing_key", signingKeyB64Encoded) + .put("encryption_key", claimsEncryptionKey) + .build(); + } + + private Settings nonSpecifyOBOSetting() { + return Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); + } +} diff --git a/src/test/resources/restapi/securityconfig_nondefault.json b/src/test/resources/restapi/securityconfig_nondefault.json index 6fb297be37..a5660c6496 100644 --- a/src/test/resources/restapi/securityconfig_nondefault.json +++ b/src/test/resources/restapi/securityconfig_nondefault.json @@ -170,6 +170,11 @@ "do_not_fail_on_forbidden" : false, "multi_rolespan_enabled" : true, "hosts_resolver_mode" : "ip-only", - "do_not_fail_on_forbidden_empty" : false + "do_not_fail_on_forbidden_empty" : false, + "on_behalf_of": { + "enabled": true, + "signing_key": "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z", + "encryption_key": "ZW5jcnlwdGlvbktleQ==" + } } }